liubao68 closed pull request #668: [SCB-487] SDK consumer download file
URL: https://github.com/apache/incubator-servicecomb-java-chassis/pull/668
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git 
a/demo/demo-schema/src/main/java/org/apache/servicecomb/demo/TestMgr.java 
b/demo/demo-schema/src/main/java/org/apache/servicecomb/demo/TestMgr.java
index c72bb3678..b0086f6f5 100644
--- a/demo/demo-schema/src/main/java/org/apache/servicecomb/demo/TestMgr.java
+++ b/demo/demo-schema/src/main/java/org/apache/servicecomb/demo/TestMgr.java
@@ -40,6 +40,10 @@ public static void setMsg(String microserviceName, String 
transport) {
   }
 
   public static void check(Object expect, Object real) {
+    check(expect, real, null);
+  }
+
+  public static void check(Object expect, Object real, Throwable error) {
     if (expect == real) {
       return;
     }
@@ -48,7 +52,11 @@ public static void check(Object expect, Object real) {
     String strReal = String.valueOf(real);
 
     if (!strExpect.equals(strReal)) {
-      errorList.add(new Error(msg + " | Expect " + strExpect + ", but " + 
strReal));
+      Error newError = new Error(msg + " | Expect " + strExpect + ", but " + 
strReal);
+      if (error != null) {
+        newError.setStackTrace(error.getStackTrace());
+      }
+      errorList.add(newError);
     }
   }
 
diff --git 
a/demo/demo-springmvc/springmvc-client/src/main/java/org/apache/servicecomb/demo/springmvc/client/CodeFirstRestTemplateSpringmvc.java
 
b/demo/demo-springmvc/springmvc-client/src/main/java/org/apache/servicecomb/demo/springmvc/client/CodeFirstRestTemplateSpringmvc.java
index f23e8822c..2303cc01b 100644
--- 
a/demo/demo-springmvc/springmvc-client/src/main/java/org/apache/servicecomb/demo/springmvc/client/CodeFirstRestTemplateSpringmvc.java
+++ 
b/demo/demo-springmvc/springmvc-client/src/main/java/org/apache/servicecomb/demo/springmvc/client/CodeFirstRestTemplateSpringmvc.java
@@ -70,8 +70,12 @@
 
   private TestGeneric testGeneric = new TestGeneric();
 
+  private TestDownload testDownload = new TestDownload();
+
   @Override
   protected void testOnlyRest(RestTemplate template, String cseUrlPrefix) {
+    testDownload.runRest();
+
     try {
       testUpload(template, cseUrlPrefix);
     } catch (IOException e) {
diff --git 
a/demo/demo-springmvc/springmvc-client/src/main/java/org/apache/servicecomb/demo/springmvc/client/DownloadSchemaIntf.java
 
b/demo/demo-springmvc/springmvc-client/src/main/java/org/apache/servicecomb/demo/springmvc/client/DownloadSchemaIntf.java
new file mode 100644
index 000000000..48b9a0f49
--- /dev/null
+++ 
b/demo/demo-springmvc/springmvc-client/src/main/java/org/apache/servicecomb/demo/springmvc/client/DownloadSchemaIntf.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicecomb.demo.springmvc.client;
+
+import org.apache.servicecomb.foundation.vertx.http.ReadStreamPart;
+
+public interface DownloadSchemaIntf {
+  ReadStreamPart tempFileEntity(String content);
+
+  ReadStreamPart tempFilePart(String content);
+
+  ReadStreamPart file(String content);
+
+  ReadStreamPart chineseAndSpaceFile(String content);
+
+  ReadStreamPart resource(String content);
+
+  ReadStreamPart entityResource(String content);
+
+  ReadStreamPart entityInputStream(String content);
+
+  ReadStreamPart netInputStream(String content);
+}
diff --git 
a/demo/demo-springmvc/springmvc-client/src/main/java/org/apache/servicecomb/demo/springmvc/client/TestDownload.java
 
b/demo/demo-springmvc/springmvc-client/src/main/java/org/apache/servicecomb/demo/springmvc/client/TestDownload.java
new file mode 100644
index 000000000..303c00142
--- /dev/null
+++ 
b/demo/demo-springmvc/springmvc-client/src/main/java/org/apache/servicecomb/demo/springmvc/client/TestDownload.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicecomb.demo.springmvc.client;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.servicecomb.demo.TestMgr;
+import org.apache.servicecomb.foundation.vertx.http.ReadStreamPart;
+import org.apache.servicecomb.provider.pojo.Invoker;
+import org.apache.servicecomb.provider.springmvc.reference.CseRestTemplate;
+import org.springframework.web.client.RestTemplate;
+
+public class TestDownload {
+  private File dir = new File("target/download");
+
+  private DownloadSchemaIntf intf = Invoker.createProxy("springmvc", 
"download", DownloadSchemaIntf.class);
+
+  private RestTemplate restTemplate = new CseRestTemplate();
+
+  private String prefix = "cse://springmvc/download";
+
+  private List<CompletableFuture<?>> futures = new ArrayList<>();
+
+  private String content = "file content";
+
+  public TestDownload() {
+    FileUtils.deleteQuietly(dir);
+  }
+
+  private String readFileToString(File file) {
+    try {
+      return FileUtils.readFileToString(file);
+    } catch (IOException e) {
+      return "read file failed:" + e.getMessage();
+    }
+  }
+
+  private CompletableFuture<File> checkFile(ReadStreamPart part) {
+    CompletableFuture<File> future = part.saveToFile("target/download/"
+        + UUID.randomUUID().toString()
+        + "-"
+        + part.getSubmittedFileName());
+    return checkFuture(future);
+  }
+
+  private <T> CompletableFuture<T> checkFuture(CompletableFuture<T> future) {
+    Error error = new Error();
+    future.whenComplete((result, e) -> {
+      Object value = result;
+      if (File.class.isInstance(value)) {
+        value = readFileToString((File) value);
+        ((File) value).delete();
+      } else if (byte[].class.isInstance(value)) {
+        value = new String((byte[]) value);
+      }
+
+      TestMgr.check(content, value, error);
+    });
+
+    return future;
+  }
+
+  private ReadStreamPart templateGet(String methodPath) {
+    return restTemplate
+        .getForObject(prefix + "/" + methodPath + "?content={content}",
+            ReadStreamPart.class,
+            content);
+  }
+
+  public void runRest() {
+    futures.add(checkFile(intf.tempFileEntity(content)));
+    futures.add(checkFuture(templateGet("tempFileEntity").saveAsBytes()));
+
+    futures.add(checkFile(intf.tempFilePart(content)));
+    futures.add(checkFuture(templateGet("tempFilePart").saveAsString()));
+
+    futures.add(checkFile(intf.file(content)));
+    futures.add(checkFuture(templateGet("file").saveAsString()));
+
+    // fix in next PR
+    //    futures.add(checkFile(intf.chineseAndSpaceFile(content)));
+    //    
futures.add(checkFuture(templateGet("chineseAndSpaceFile").saveAsString()));
+
+    futures.add(checkFile(intf.resource(content)));
+    futures.add(checkFuture(templateGet("resource").saveAsString()));
+
+    futures.add(checkFile(intf.entityResource(content)));
+    futures.add(checkFuture(templateGet("entityResource").saveAsString()));
+
+    futures.add(checkFile(intf.entityInputStream(content)));
+    futures.add(checkFuture(templateGet("entityInputStream").saveAsString()));
+
+    futures.add(checkFile(intf.netInputStream(content)));
+    futures.add(checkFuture(templateGet("netInputStream").saveAsString()));
+
+    try {
+      CompletableFuture
+          .allOf(futures.toArray(new CompletableFuture[futures.size()]))
+          .get();
+    } catch (InterruptedException | ExecutionException e1) {
+    }
+  }
+}
diff --git 
a/demo/demo-springmvc/springmvc-server/src/main/java/org/apache/servicecomb/demo/springmvc/server/DownloadSchema.java
 
b/demo/demo-springmvc/springmvc-server/src/main/java/org/apache/servicecomb/demo/springmvc/server/DownloadSchema.java
index 712551189..c6124d204 100644
--- 
a/demo/demo-springmvc/springmvc-server/src/main/java/org/apache/servicecomb/demo/springmvc/server/DownloadSchema.java
+++ 
b/demo/demo-springmvc/springmvc-server/src/main/java/org/apache/servicecomb/demo/springmvc/server/DownloadSchema.java
@@ -20,17 +20,26 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.util.UUID;
 
 import javax.servlet.http.Part;
 
 import org.apache.commons.io.FileUtils;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.bootstrap.HttpServer;
+import org.apache.http.impl.bootstrap.ServerBootstrap;
 import org.apache.servicecomb.foundation.common.part.FilePart;
 import org.apache.servicecomb.provider.rest.common.RestSchema;
 import org.springframework.core.io.ByteArrayResource;
 import org.springframework.core.io.Resource;
 import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -41,24 +50,45 @@
 @RestSchema(schemaId = "download")
 @RequestMapping(path = "/download")
 public class DownloadSchema {
-  File tempDir = new File("downloadTemp");
+  File tempDir = new File("target/downloadTemp");
 
   public DownloadSchema() throws IOException {
+    FileUtils.deleteQuietly(tempDir);
     FileUtils.forceMkdir(tempDir);
+
+    // for download from net stream case
+    HttpServer server = ServerBootstrap
+        .bootstrap()
+        .setListenerPort(9000)
+        .registerHandler("/download/netInputStream", (req, resp, context) -> {
+          String uri = req.getRequestLine().getUri();
+          String query = URI.create(uri).getQuery();
+          int idx = query.indexOf('=');
+          String content = query.substring(idx + 1);
+          content = URLDecoder.decode(content, StandardCharsets.UTF_8.name());
+          resp.setEntity(new StringEntity(content, 
StandardCharsets.UTF_8.name()));
+        })
+        .create();
+    server.start();
+  }
+
+  protected File createTempFile(String content) throws IOException {
+    return createTempFile(null, content);
   }
 
-  // content is file name
-  protected File createTempFile() throws IOException {
-    String name = "download-" + UUID.randomUUID().toString() + ".txt";
+  protected File createTempFile(String name, String content) throws 
IOException {
+    if (name == null) {
+      name = "download-" + UUID.randomUUID().toString() + ".txt";
+    }
     File file = new File(tempDir, name);
-    FileUtils.write(file, name);
+    FileUtils.write(file, content);
     return file;
   }
 
   // customize HttpHeaders.CONTENT_DISPOSITION to be 
"attachment;filename=tempFileEntity.txt"
   @GetMapping(path = "/tempFileEntity")
-  public ResponseEntity<Part> downloadTempFileEntity() throws IOException {
-    File file = createTempFile();
+  public ResponseEntity<Part> tempFileEntity(String content) throws 
IOException {
+    File file = createTempFile(content);
 
     return ResponseEntity
         .ok()
@@ -69,8 +99,8 @@ protected File createTempFile() throws IOException {
 
   // generate HttpHeaders.CONTENT_DISPOSITION to be 
"attachment;filename=tempFilePart.txt" automatically
   @GetMapping(path = "/tempFilePart")
-  public Part downloadTempFilePart() throws IOException {
-    File file = createTempFile();
+  public Part tempFilePart(String content) throws IOException {
+    File file = createTempFile(content);
 
     return new FilePart(null, file)
         .setDeleteAfterFinished(true)
@@ -78,19 +108,27 @@ public Part downloadTempFilePart() throws IOException {
   }
 
   @GetMapping(path = "/file")
-  public File downloadFile() throws IOException {
-    return new 
File(this.getClass().getClassLoader().getResource("microservice.yaml").getFile());
+  public File file(String content) throws IOException {
+    return createTempFile("file.txt", content);
+  }
+
+  @GetMapping(path = "/chineseAndSpaceFile")
+  public Part chineseAndSpaceFile(String content) throws IOException {
+    File file = createTempFile(content);
+    return new FilePart(null, file)
+        .setDeleteAfterFinished(true)
+        .setSubmittedFileName("测 试.test.txt");
   }
 
   @GetMapping(path = "/resource")
   @ApiResponses({
       @ApiResponse(code = 200, response = File.class, message = ""),
   })
-  public Resource downloadResource() throws IOException {
-    return new ByteArrayResource("abc".getBytes(StandardCharsets.UTF_8)) {
+  public Resource resource(String content) throws IOException {
+    return new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8)) {
       @Override
       public String getFilename() {
-        return "abc.txt";
+        return "resource.txt";
       }
     };
   }
@@ -99,21 +137,38 @@ public String getFilename() {
   @ApiResponses({
       @ApiResponse(code = 200, response = File.class, message = ""),
   })
-  public ResponseEntity<Resource> downloadEntityResource() throws IOException {
+  public ResponseEntity<Resource> entityResource(String content) throws 
IOException {
     return ResponseEntity
         .ok()
+        .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE)
         .header(HttpHeaders.CONTENT_DISPOSITION, 
"attachment;filename=entityResource.txt")
-        .body(new 
ByteArrayResource("entityResource".getBytes(StandardCharsets.UTF_8)));
+        .body(new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8)));
   }
 
   @GetMapping(path = "/entityInputStream")
   @ApiResponses({
       @ApiResponse(code = 200, response = File.class, message = ""),
   })
-  public ResponseEntity<InputStream> downloadEntityInputStream() throws 
IOException {
+  public ResponseEntity<InputStream> entityInputStream(String content) throws 
IOException {
     return ResponseEntity
         .ok()
+        .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE)
         .header(HttpHeaders.CONTENT_DISPOSITION, 
"attachment;filename=entityInputStream.txt")
-        .body(new 
ByteArrayInputStream("entityInputStream".getBytes(StandardCharsets.UTF_8)));
+        .body(new 
ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
+  }
+
+  @GetMapping(path = "/netInputStream")
+  @ApiResponses({
+      @ApiResponse(code = 200, response = File.class, message = ""),
+  })
+  public ResponseEntity<InputStream> netInputStream(String content) throws 
IOException {
+    URL url = new URL("http://localhost:9000/download/netInputStream?content=";
+        + URLEncoder.encode(content, StandardCharsets.UTF_8.name()));
+    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+    return ResponseEntity
+        .ok()
+        .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE)
+        .header(HttpHeaders.CONTENT_DISPOSITION, 
"attachment;filename=netInputStream.txt")
+        .body(conn.getInputStream());
   }
 }
diff --git a/demo/pom.xml b/demo/pom.xml
index 55505d2ad..4724b6c07 100644
--- a/demo/pom.xml
+++ b/demo/pom.xml
@@ -221,7 +221,7 @@
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-jar-plugin</artifactId>
               <configuration>
-                
<outputDirectory>${demo.run.dir}/${project.name}</outputDirectory>
+                
<outputDirectory>${demo.run.dir}/${project.artifactId}</outputDirectory>
                 <archive>
                   <manifest>
                     <addClasspath>true</addClasspath>
diff --git 
a/foundations/foundation-common/src/main/java/org/apache/servicecomb/foundation/common/http/HttpUtils.java
 
b/foundations/foundation-common/src/main/java/org/apache/servicecomb/foundation/common/http/HttpUtils.java
new file mode 100644
index 000000000..deb6a977d
--- /dev/null
+++ 
b/foundations/foundation-common/src/main/java/org/apache/servicecomb/foundation/common/http/HttpUtils.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicecomb.foundation.common.http;
+
+import java.io.File;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+import org.springframework.util.StringUtils;
+
+public final class HttpUtils {
+  private HttpUtils() {
+  }
+
+  /**
+   * paramName is not case sensitive
+   * @param headerValue example: attachment;filename=a.txt
+   * 
+   */
+  // 
+  public static String parseParamFromHeaderValue(String headerValue, String 
paramName) {
+    if (StringUtils.isEmpty(headerValue)) {
+      return null;
+    }
+
+    for (String value : headerValue.split(";")) {
+      int idx = value.indexOf('=');
+      if (idx == -1) {
+        continue;
+      }
+
+      if (paramName.equalsIgnoreCase(value.substring(0, idx))) {
+        return value.substring(idx + 1);
+      }
+    }
+    return null;
+  }
+
+  public static String uriEncode(String value) {
+    return uriEncode(value, StandardCharsets.UTF_8.name());
+  }
+
+  public static String uriEncode(String value, String enc) {
+    try {
+      return URLEncoder.encode(value, enc).replace("+", "%20");
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalStateException(String.format("uriEncode failed, 
value=\"%s\", enc=\"%s\".", value, enc), e);
+    }
+  }
+
+  public static String uriDecode(String value) {
+    return uriDecode(value, StandardCharsets.UTF_8.name());
+  }
+
+  public static String uriDecode(String value, String enc) {
+    try {
+      return URLDecoder.decode(value, enc);
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalStateException(String.format("uriDecode failed, 
value=\"%s\", enc=\"%s\".", value, enc), e);
+    }
+  }
+
+  public static String parseFileNameFromHeaderValue(String headerValue) {
+    String fileName = parseParamFromHeaderValue(headerValue, "filename");
+    fileName = StringUtils.isEmpty(fileName) ? "default" : fileName;
+    fileName = uriDecode(fileName);
+    return new File(fileName).getName();
+  }
+}
diff --git 
a/foundations/foundation-common/src/test/java/org/apache/servicecomb/foundation/common/http/TestHttpUtils.java
 
b/foundations/foundation-common/src/test/java/org/apache/servicecomb/foundation/common/http/TestHttpUtils.java
new file mode 100644
index 000000000..18f8906cf
--- /dev/null
+++ 
b/foundations/foundation-common/src/test/java/org/apache/servicecomb/foundation/common/http/TestHttpUtils.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicecomb.foundation.common.http;
+
+import java.io.UnsupportedEncodingException;
+
+import org.hamcrest.Matchers;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+
+public class TestHttpUtils {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void parseParamFromHeaderValue_normal() {
+    Assert.assertEquals("v", HttpUtils.parseParamFromHeaderValue("xx;k=v", 
"k"));
+  }
+
+  @Test
+  public void parseParamFromHeaderValue_normal_ignoreCase() {
+    Assert.assertEquals("v", HttpUtils.parseParamFromHeaderValue("xx;K=v", 
"k"));
+  }
+
+  @Test
+  public void parseParamFromHeaderValue_null() {
+    Assert.assertNull(HttpUtils.parseParamFromHeaderValue(null, "k"));
+  }
+
+  @Test
+  public void parseParamFromHeaderValue_noKv() {
+    Assert.assertNull(HttpUtils.parseParamFromHeaderValue("xx", "k"));
+  }
+
+  @Test
+  public void parseParamFromHeaderValue_noV() {
+    Assert.assertEquals("", HttpUtils.parseParamFromHeaderValue("xx;k=", "k"));
+  }
+
+  @Test
+  public void parseParamFromHeaderValue_keyNotFound() {
+    Assert.assertNull(HttpUtils.parseParamFromHeaderValue("xx;k=", "kk"));
+  }
+
+  @Test
+  public void uriEncode_chineseAndSpace() {
+    String encoded = HttpUtils.uriEncode("测 试");
+    Assert.assertEquals("%E6%B5%8B%20%E8%AF%95", encoded);
+    Assert.assertEquals("测 试", HttpUtils.uriDecode(encoded));
+  }
+
+  @Test
+  public void uriEncode_failed() {
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage(Matchers.is("uriEncode failed, value=\"\", 
enc=\"notExistEnc\"."));
+    
expectedException.expectCause(Matchers.instanceOf(UnsupportedEncodingException.class));
+
+    HttpUtils.uriEncode("", "notExistEnc");
+  }
+
+  @Test
+  public void uriDecode_failed() {
+    expectedException.expect(IllegalStateException.class);
+    expectedException
+        .expectMessage(Matchers.is("uriDecode failed, 
value=\"%E6%B5%8B%20%E8%AF%95\", enc=\"notExistEnc\"."));
+    
expectedException.expectCause(Matchers.instanceOf(UnsupportedEncodingException.class));
+
+    HttpUtils.uriDecode("%E6%B5%8B%20%E8%AF%95", "notExistEnc");
+  }
+
+  @Test
+  public void parseFileNameFromHeaderValue() {
+    String fileName = "测 试.txt";
+    String encoded = HttpUtils.uriEncode(fileName);
+    Assert.assertEquals(fileName, 
HttpUtils.parseFileNameFromHeaderValue("xx;filename=" + encoded));
+  }
+
+  @Test
+  public void parseFileNameFromHeaderValue_defaultName() {
+    Assert.assertEquals("default", 
HttpUtils.parseFileNameFromHeaderValue("xx"));
+  }
+
+  @Test
+  public void parseFileNameFromHeaderValue_ignorePath() {
+    Assert.assertEquals("a.txt", 
HttpUtils.parseFileNameFromHeaderValue("xx;filename=../../a.txt"));
+  }
+}
diff --git 
a/foundations/foundation-vertx/src/main/java/org/apache/servicecomb/foundation/vertx/http/ReadStreamPart.java
 
b/foundations/foundation-vertx/src/main/java/org/apache/servicecomb/foundation/vertx/http/ReadStreamPart.java
index 401460992..ddc56c9e3 100644
--- 
a/foundations/foundation-vertx/src/main/java/org/apache/servicecomb/foundation/vertx/http/ReadStreamPart.java
+++ 
b/foundations/foundation-vertx/src/main/java/org/apache/servicecomb/foundation/vertx/http/ReadStreamPart.java
@@ -16,13 +16,26 @@
  */
 package org.apache.servicecomb.foundation.vertx.http;
 
+import java.io.File;
 import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
 
 import javax.servlet.http.Part;
+import javax.ws.rs.core.HttpHeaders;
 
+import org.apache.commons.lang.StringUtils;
+import org.apache.servicecomb.foundation.common.http.HttpUtils;
 import org.apache.servicecomb.foundation.common.part.AbstractPart;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Context;
+import io.vertx.core.Vertx;
 import io.vertx.core.buffer.Buffer;
+import io.vertx.core.file.AsyncFile;
+import io.vertx.core.file.OpenOptions;
+import io.vertx.core.http.HttpClientResponse;
 import io.vertx.core.streams.Pump;
 import io.vertx.core.streams.ReadStream;
 import io.vertx.core.streams.WriteStream;
@@ -36,9 +49,26 @@
  * {@link 
org.apache.servicecomb.foundation.vertx.http.VertxServerResponseToHttpServletResponse#sendPart(Part)
 VertxServerResponseToHttpServletResponse.sendPart}
  */
 public class ReadStreamPart extends AbstractPart {
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(ReadStreamPart.class);
+
+  private Context context;
+
   private ReadStream<Buffer> readStream;
 
-  public ReadStreamPart(ReadStream<Buffer> readStream) {
+  public ReadStreamPart(Context context, HttpClientResponse 
httpClientResponse) {
+    this(context, (ReadStream<Buffer>) httpClientResponse);
+
+    setSubmittedFileName(
+        
HttpUtils.parseFileNameFromHeaderValue(httpClientResponse.getHeader(HttpHeaders.CONTENT_DISPOSITION)));
+
+    String contentType = 
httpClientResponse.getHeader(HttpHeaders.CONTENT_TYPE);
+    if (StringUtils.isNotEmpty(contentType)) {
+      this.contentType(contentType);
+    }
+  }
+
+  public ReadStreamPart(Context context, ReadStream<Buffer> readStream) {
+    this.context = context;
     this.readStream = readStream;
 
     readStream.pause();
@@ -60,4 +90,75 @@ public ReadStreamPart(ReadStream<Buffer> readStream) {
 
     return future;
   }
+
+  public CompletableFuture<byte[]> saveAsBytes() {
+    return saveAs(buf -> {
+      return buf.getBytes();
+    });
+  }
+
+  public CompletableFuture<String> saveAsString() {
+    return saveAs(buf -> {
+      return buf.toString();
+    });
+  }
+
+  public <T> CompletableFuture<T> saveAs(Function<Buffer, T> converter) {
+    CompletableFuture<T> future = new CompletableFuture<>();
+    Buffer buffer = Buffer.buffer();
+
+    readStream.exceptionHandler(future::completeExceptionally);
+    readStream.handler(buffer::appendBuffer);
+    readStream.endHandler(v -> {
+      future.complete(converter.apply(buffer));
+    });
+    readStream.resume();
+
+    return future;
+  }
+
+  public CompletableFuture<File> saveToFile(String fileName) {
+    File file = new File(fileName);
+    file.getParentFile().mkdirs();
+    OpenOptions openOptions = new OpenOptions().setCreateNew(true);
+    return saveToFile(file, openOptions);
+  }
+
+  public CompletableFuture<File> saveToFile(File file, OpenOptions 
openOptions) {
+    CompletableFuture<File> future = new CompletableFuture<>();
+
+    Vertx vertx = context.owner();
+    vertx.fileSystem().open(file.getAbsolutePath(), openOptions, ar -> {
+      onFileOpened(file, ar, future);
+    });
+
+    return future;
+  }
+
+  protected void onFileOpened(File file, AsyncResult<AsyncFile> ar, 
CompletableFuture<File> future) {
+    if (ar.failed()) {
+      future.completeExceptionally(ar.cause());
+      return;
+    }
+
+    AsyncFile asyncFile = ar.result();
+    CompletableFuture<Void> saveFuture = saveToWriteStream(asyncFile);
+    saveFuture.whenComplete((v, saveException) -> {
+      asyncFile.close(closeAr -> {
+        if (closeAr.failed()) {
+          LOGGER.error("Failed to close file {}.", file);
+        }
+
+        // whatever close success or failed
+        // will not affect to result
+        // result just only related to write
+        if (saveException == null) {
+          future.complete(file);
+          return;
+        }
+
+        future.completeExceptionally(saveException);
+      });
+    });
+  }
 }
diff --git 
a/foundations/foundation-vertx/src/test/java/io/vertx/core/file/impl/AsyncFileUitls.java
 
b/foundations/foundation-vertx/src/test/java/io/vertx/core/file/impl/AsyncFileUitls.java
new file mode 100644
index 000000000..a458f9bd6
--- /dev/null
+++ 
b/foundations/foundation-vertx/src/test/java/io/vertx/core/file/impl/AsyncFileUitls.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.vertx.core.file.impl;
+
+import io.vertx.core.file.AsyncFile;
+import io.vertx.core.file.OpenOptions;
+import io.vertx.core.impl.ContextImpl;
+import io.vertx.core.impl.VertxInternal;
+
+public class AsyncFileUitls {
+  public static AsyncFile createAsyncFile(VertxInternal vertx, String path, 
OpenOptions options, ContextImpl context) {
+    return new AsyncFileImpl(vertx, path, options, context);
+  }
+}
diff --git 
a/foundations/foundation-vertx/src/test/java/org/apache/servicecomb/foundation/vertx/http/TestReadStreamPart.java
 
b/foundations/foundation-vertx/src/test/java/org/apache/servicecomb/foundation/vertx/http/TestReadStreamPart.java
index cffd96498..0f9e730d9 100644
--- 
a/foundations/foundation-vertx/src/test/java/org/apache/servicecomb/foundation/vertx/http/TestReadStreamPart.java
+++ 
b/foundations/foundation-vertx/src/test/java/org/apache/servicecomb/foundation/vertx/http/TestReadStreamPart.java
@@ -17,10 +17,15 @@
 package org.apache.servicecomb.foundation.vertx.http;
 
 import java.io.ByteArrayInputStream;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 
+import javax.ws.rs.core.HttpHeaders;
+
+import org.apache.commons.io.FileUtils;
 import org.apache.servicecomb.foundation.vertx.stream.InputStreamToReadStream;
 import org.hamcrest.Matchers;
 import org.junit.Assert;
@@ -30,18 +35,35 @@
 import org.junit.rules.ExpectedException;
 
 import io.vertx.core.AsyncResult;
+import io.vertx.core.Context;
 import io.vertx.core.Future;
 import io.vertx.core.Handler;
 import io.vertx.core.Vertx;
 import io.vertx.core.buffer.Buffer;
+import io.vertx.core.file.AsyncFile;
+import io.vertx.core.file.FileSystem;
+import io.vertx.core.file.FileSystemException;
+import io.vertx.core.file.OpenOptions;
+import io.vertx.core.file.impl.AsyncFileUitls;
+import io.vertx.core.file.impl.FileSystemImpl;
+import io.vertx.core.file.impl.WindowsFileSystem;
+import io.vertx.core.http.HttpClientResponse;
+import io.vertx.core.impl.ContextImpl;
+import io.vertx.core.impl.EventLoopContext;
+import io.vertx.core.impl.Utils;
+import io.vertx.core.impl.VertxInternal;
 import io.vertx.core.streams.WriteStream;
+import mockit.Expectations;
 import mockit.Mock;
 import mockit.MockUp;
 import mockit.Mocked;
 
 public class TestReadStreamPart {
   @Mocked
-  Vertx vertx;
+  VertxInternal vertx;
+
+  //  @Mocked
+  ContextImpl context;
 
   String src = "src";
 
@@ -54,13 +76,30 @@
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
 
+  FileSystem fileSystem;
+
+  protected FileSystem getFileSystem() {
+    return Utils.isWindows() ? new WindowsFileSystem(vertx) : new 
FileSystemImpl(vertx);
+  }
 
   @Before
   public void setup() {
-    readStream = new InputStreamToReadStream(vertx, inputStream);
-    part = new ReadStreamPart(readStream);
-
     new MockUp<Vertx>(vertx) {
+      @Mock
+      FileSystem fileSystem() {
+        return fileSystem;
+      }
+
+      @Mock
+      ContextImpl getContext() {
+        return context;
+      }
+
+      @Mock
+      ContextImpl getOrCreateContext() {
+        return context;
+      }
+
       @Mock
       <T> void executeBlocking(Handler<Future<T>> blockingCodeHandler, boolean 
ordered,
           Handler<AsyncResult<T>> resultHandler) {
@@ -69,6 +108,78 @@ public void setup() {
         future.setHandler(resultHandler);
       }
     };
+
+    context = new EventLoopContext(vertx, null, null, null, "id", null, null);
+    new MockUp<Context>(context) {
+      @Mock
+      Vertx owner() {
+        return vertx;
+      }
+
+      @Mock
+      void runOnContext(Handler<Void> task) {
+        task.handle(null);
+      }
+
+      @Mock
+      <T> void executeBlocking(Handler<Future<T>> blockingCodeHandler, 
Handler<AsyncResult<T>> resultHandler) {
+        Future<T> future = Future.future();
+        blockingCodeHandler.handle(future);
+        future.setHandler(resultHandler);
+      }
+    };
+
+    fileSystem = getFileSystem();
+
+    readStream = new InputStreamToReadStream(vertx, inputStream);
+    part = new ReadStreamPart(context, readStream);
+
+    new MockUp<FileSystem>(fileSystem) {
+      @Mock
+      FileSystem open(String path, OpenOptions options, 
Handler<AsyncResult<AsyncFile>> handler) {
+        try {
+          AsyncFile asyncFile = AsyncFileUitls.createAsyncFile(vertx, path, 
options, context);
+          handler.handle(Future.succeededFuture(asyncFile));
+        } catch (Exception e) {
+          handler.handle(Future.failedFuture(e));
+        }
+        return fileSystem;
+      }
+    };
+  }
+
+  @Test
+  public void constructFromHttpClientResponse_noContentType(@Mocked 
HttpClientResponse httpClientResponse) {
+    new Expectations() {
+      {
+        httpClientResponse.getHeader(HttpHeaders.CONTENT_DISPOSITION);
+        result = "xx;filename=name.txt";
+        httpClientResponse.getHeader(HttpHeaders.CONTENT_TYPE);
+        result = null;
+      }
+    };
+
+    part = new ReadStreamPart(context, httpClientResponse);
+
+    Assert.assertEquals("name.txt", part.getSubmittedFileName());
+    Assert.assertEquals("text/plain", part.getContentType());
+  }
+
+  @Test
+  public void constructFromHttpClientResponse_hasContentType(@Mocked 
HttpClientResponse httpClientResponse) {
+    new Expectations() {
+      {
+        httpClientResponse.getHeader(HttpHeaders.CONTENT_DISPOSITION);
+        result = "xx;filename=name.txt";
+        httpClientResponse.getHeader(HttpHeaders.CONTENT_TYPE);
+        result = "type";
+      }
+    };
+
+    part = new ReadStreamPart(context, httpClientResponse);
+
+    Assert.assertEquals("name.txt", part.getSubmittedFileName());
+    Assert.assertEquals("type", part.getContentType());
   }
 
   @Test
@@ -128,4 +239,43 @@ int read(byte b[]) throws IOException {
 
     part.saveToWriteStream(writeStream).get();
   }
+
+  @Test
+  public void saveAsBytes() throws InterruptedException, ExecutionException {
+    Assert.assertArrayEquals(src.getBytes(), part.saveAsBytes().get());
+  }
+
+  @Test
+  public void saveAsString() throws InterruptedException, ExecutionException {
+    Assert.assertEquals(src, part.saveAsString().get());
+  }
+
+  @Test
+  public void saveToFile() throws InterruptedException, ExecutionException, 
IOException {
+    File dir = new File("target/notExist-" + UUID.randomUUID().toString());
+    File file = new File(dir, "a.txt");
+
+    Assert.assertFalse(dir.exists());
+
+    part.saveToFile(file.getAbsolutePath()).get();
+
+    Assert.assertEquals(src, FileUtils.readFileToString(file));
+
+    FileUtils.forceDelete(dir);
+    Assert.assertFalse(dir.exists());
+  }
+
+  @Test
+  public void saveToFile_notExist_notCreate() throws InterruptedException, 
ExecutionException, IOException {
+    File dir = new File("target/notExist-" + UUID.randomUUID().toString());
+    File file = new File(dir, "a.txt");
+
+    Assert.assertFalse(dir.exists());
+
+    expectedException.expect(ExecutionException.class);
+    
expectedException.expectCause(Matchers.instanceOf(FileSystemException.class));
+
+    OpenOptions openOptions = new OpenOptions().setCreateNew(false);
+    part.saveToFile(file, openOptions).get();
+  }
 }
diff --git 
a/service-registry/src/main/java/org/apache/servicecomb/serviceregistry/registry/RemoteServiceRegistry.java
 
b/service-registry/src/main/java/org/apache/servicecomb/serviceregistry/registry/RemoteServiceRegistry.java
index 2a6047285..7e683d35d 100644
--- 
a/service-registry/src/main/java/org/apache/servicecomb/serviceregistry/registry/RemoteServiceRegistry.java
+++ 
b/service-registry/src/main/java/org/apache/servicecomb/serviceregistry/registry/RemoteServiceRegistry.java
@@ -82,11 +82,11 @@ public void run() {
         serviceRegistryConfig.getHeartbeatInterval(),
         TimeUnit.SECONDS);
 
-      taskPool.scheduleAtFixedRate(
-          () -> eventBus.post(new PeriodicPullEvent()),
-          serviceRegistryConfig.getInstancePullInterval(),
-          serviceRegistryConfig.getInstancePullInterval(),
-          TimeUnit.SECONDS);
+    taskPool.scheduleAtFixedRate(
+        () -> eventBus.post(new PeriodicPullEvent()),
+        serviceRegistryConfig.getInstancePullInterval(),
+        serviceRegistryConfig.getInstancePullInterval(),
+        TimeUnit.SECONDS);
 
   }
 
diff --git 
a/service-registry/src/test/java/org/apache/servicecomb/serviceregistry/registry/TestServiceRegistryFactory.java
 
b/service-registry/src/test/java/org/apache/servicecomb/serviceregistry/registry/TestServiceRegistryFactory.java
index 1e62441a5..07f7e08f2 100644
--- 
a/service-registry/src/test/java/org/apache/servicecomb/serviceregistry/registry/TestServiceRegistryFactory.java
+++ 
b/service-registry/src/test/java/org/apache/servicecomb/serviceregistry/registry/TestServiceRegistryFactory.java
@@ -31,13 +31,17 @@
 import com.google.common.eventbus.EventBus;
 
 import mockit.Deencapsulation;
+import mockit.Mocked;
 
 /**
  * Created by   on 2017/3/31.
  */
 public class TestServiceRegistryFactory {
   @Test
-  public void testGetRemoteRegistryClient() {
+  // mock ServiceRegistryClientImpl to avoid send request to remote SC
+  // even there is no any reference to registryClient, DO NOT delete it.
+  // because what changed is class ServiceRegistryClientImpl
+  public void testGetRemoteRegistryClient(@Mocked ServiceRegistryClientImpl 
registryClient) {
     EventBus eventBus = new EventBus();
     ServiceRegistryConfig serviceRegistryConfig = 
ServiceRegistryConfig.INSTANCE;
     MicroserviceDefinition microserviceDefinition = new 
MicroserviceDefinition(Collections.emptyList());
diff --git 
a/transports/transport-rest/transport-rest-client/src/main/java/org/apache/servicecomb/transport/rest/client/http/RestClientInvocation.java
 
b/transports/transport-rest/transport-rest-client/src/main/java/org/apache/servicecomb/transport/rest/client/http/RestClientInvocation.java
index 9a586a134..db148dd80 100644
--- 
a/transports/transport-rest/transport-rest-client/src/main/java/org/apache/servicecomb/transport/rest/client/http/RestClientInvocation.java
+++ 
b/transports/transport-rest/transport-rest-client/src/main/java/org/apache/servicecomb/transport/rest/client/http/RestClientInvocation.java
@@ -155,7 +155,7 @@ protected void handleResponse(HttpClientResponse 
httpClientResponse) {
 
     if (HttpStatus.isSuccess(clientResponse.statusCode())
         && 
Part.class.equals(invocation.getOperationMeta().getMethod().getReturnType())) {
-      ReadStreamPart part = new ReadStreamPart(httpClientResponse);
+      ReadStreamPart part = new 
ReadStreamPart(httpClientWithContext.context(), httpClientResponse);
       invocation.getHandlerContext().put(RestConst.READ_STREAM_PART, part);
       processResponseBody(null);
       return;


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
us...@infra.apache.org


With regards,
Apache Git Services

Reply via email to