blob: 2ca0701b064094e5f0330535e75482ed4e39b2c0 [file] [log] [blame]
/*
* 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.common.rest.codec.param;
import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
import static javax.ws.rs.core.MediaType.MULTIPART_FORM_DATA;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import javax.servlet.http.Part;
import javax.ws.rs.core.MediaType;
import com.google.common.annotations.VisibleForTesting;
import org.apache.servicecomb.common.rest.codec.RestClientRequest;
import org.apache.servicecomb.common.rest.codec.RestObjectMapperFactory;
import org.apache.servicecomb.foundation.common.utils.PartUtils;
import org.apache.servicecomb.foundation.vertx.stream.BufferInputStream;
import org.apache.servicecomb.foundation.vertx.stream.BufferOutputStream;
import org.apache.servicecomb.foundation.vertx.stream.InputStreamToReadStream;
import org.apache.servicecomb.foundation.vertx.stream.PumpFromPart;
import org.apache.servicecomb.swagger.invocation.AsyncResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import io.vertx.core.Context;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpHeaders;
public class RestClientRequestImpl implements RestClientRequest {
private static final Logger LOGGER = LoggerFactory.getLogger(RestClientRequestImpl.class);
protected Context context;
protected AsyncResponse asyncResp;
@VisibleForTesting
final Multimap<String, Part> uploads = ArrayListMultimap.create();
protected HttpClientRequest request;
protected Map<String, String> cookieMap;
protected Map<String, Object> formMap;
protected Buffer bodyBuffer;
private final Handler<Throwable> throwableHandler;
public RestClientRequestImpl(HttpClientRequest request, Context context, AsyncResponse asyncResp) {
this(request, context, asyncResp, null);
}
public RestClientRequestImpl(HttpClientRequest request, Context context, AsyncResponse asyncResp,
Handler<Throwable> throwableHandler) {
this.context = context;
this.asyncResp = asyncResp;
this.request = request;
this.throwableHandler = throwableHandler;
}
@Override
public void write(Buffer bodyBuffer) {
this.bodyBuffer = bodyBuffer;
}
@Override
public Buffer getBodyBuffer() throws Exception {
genBodyBuffer();
return bodyBuffer;
}
@Override
@SuppressWarnings("unchecked")
public void attach(String name, Object partOrList) {
if (null == partOrList) {
LOGGER.debug("null file is ignored, file name = [{}]", name);
return;
}
if (partOrList.getClass().isArray()) {
for (Object part : (Object[]) partOrList) {
uploads.put(name, PartUtils.getSinglePart(name, part));
}
}
if (List.class.isAssignableFrom(partOrList.getClass())) {
for (Object part : (List<Object>) partOrList) {
uploads.put(name, PartUtils.getSinglePart(name, part));
}
return;
}
uploads.put(name, PartUtils.getSinglePart(name, partOrList));
}
@Override
public Future<Void> end() {
writeCookies();
if (!uploads.isEmpty()) {
return doEndWithUpload();
}
return doEndNormal();
}
protected Future<Void> doEndWithUpload() {
request.setChunked(true);
String boundary = "boundary" + UUID.randomUUID();
putHeader(CONTENT_TYPE, MULTIPART_FORM_DATA + "; charset=UTF-8; boundary=" + boundary);
return genBodyForm(boundary).onSuccess(v -> attachFiles(boundary)).onFailure(e -> asyncResp.consumerFail(e));
}
private Future<Void> genBodyForm(String boundary) {
if (formMap == null) {
return Future.succeededFuture();
}
try {
try (BufferOutputStream output = new BufferOutputStream()) {
for (Entry<String, Object> entry : formMap.entrySet()) {
output.write(bytesOf("\r\n"));
output.write(bytesOf("--" + boundary + "\r\n"));
output.write(bytesOf("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"\r\n\r\n"));
if (entry.getValue() != null) {
String value = RestObjectMapperFactory.getRestObjectMapper().convertToString(entry.getValue());
output.write(value.getBytes(StandardCharsets.UTF_8));
}
}
return writeBuffer(output.getBuffer());
}
} catch (Exception e) {
return Future.failedFuture(e);
}
}
private byte[] bytesOf(String string) {
return string.getBytes(StandardCharsets.UTF_8);
}
protected Future<Void> doEndNormal() {
try {
genBodyBuffer();
} catch (Exception e) {
asyncResp.consumerFail(e);
return Future.succeededFuture();
}
if (bodyBuffer == null) {
return request.end();
}
return request.end(bodyBuffer);
}
private void attachFiles(String boundary) {
Iterator<Entry<String, Part>> uploadsIterator = uploads.entries().iterator();
attachFile(boundary, uploadsIterator);
}
private void attachFile(String boundary, Iterator<Entry<String, Part>> uploadsIterator) {
if (!uploadsIterator.hasNext()) {
writeBuffer(boundaryEndInfo(boundary)).onSuccess(v -> request.end()).onFailure(e -> asyncResp.consumerFail(e));
return;
}
Entry<String, Part> entry = uploadsIterator.next();
// do not use part.getName() to get parameter name
// because pojo consumer not easy to set name to part
String name = entry.getKey();
Part part = entry.getValue();
String filename = part.getSubmittedFileName();
LOGGER.debug("Start attach file [{}:{}].", name, filename);
writeBuffer(fileBoundaryInfo(boundary, name, part)).onSuccess(r ->
new PumpFromPart(context, part).toWriteStream(request, throwableHandler).whenComplete((v, e) -> {
if (e != null) {
LOGGER.warn("Failed attach file [{}:{}].", name, filename, e);
asyncResp.consumerFail(e);
return;
}
LOGGER.debug("Finish attach file [{}:{}].", name, filename);
attachFile(boundary, uploadsIterator);
})).onFailure(e -> asyncResp.consumerFail(e));
}
private Buffer boundaryEndInfo(String boundary) {
return Buffer.buffer()
.appendString("\r\n")
.appendString("--" + boundary + "--\r\n");
}
protected Buffer fileBoundaryInfo(String boundary, String name, Part part) {
Buffer buffer = Buffer.buffer();
buffer.appendString("\r\n");
buffer.appendString("--" + boundary + "\r\n");
buffer.appendString("Content-Disposition: form-data; name=\"")
.appendString(name)
.appendString("\"; filename=\"")
.appendString(part.getSubmittedFileName() != null ? part.getSubmittedFileName() : "null")
.appendString("\"\r\n");
buffer.appendString("Content-Type: ").appendString(part.getContentType()).appendString("\r\n");
buffer.appendString("Content-Transfer-Encoding: binary\r\n");
buffer.appendString("\r\n");
return buffer;
}
protected Future<Void> writeBuffer(Buffer buffer) {
return new InputStreamToReadStream(context,
new BufferInputStream(buffer.getByteBuf()), true).pipe().endOnComplete(false).to(request);
}
private void genBodyBuffer() throws Exception {
if (bodyBuffer != null) {
return;
}
if (formMap == null) {
return;
}
request.putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
try (BufferOutputStream output = new BufferOutputStream()) {
for (Entry<String, Object> entry : formMap.entrySet()) {
output.write(entry.getKey().getBytes(StandardCharsets.UTF_8));
output.write('=');
if (entry.getValue() != null) {
String value = RestObjectMapperFactory.getRestObjectMapper().convertToString(entry.getValue());
value = URLEncoder.encode(value, StandardCharsets.UTF_8.name());
output.write(value.getBytes(StandardCharsets.UTF_8));
}
output.write('&');
}
bodyBuffer = output.getBuffer();
}
}
private void writeCookies() {
if (cookieMap == null) {
return;
}
StringBuilder builder = new StringBuilder();
for (Entry<String, String> entry : cookieMap.entrySet()) {
builder.append(entry.getKey())
.append('=')
.append(entry.getValue())
.append("; ");
}
request.putHeader(HttpHeaders.COOKIE, builder.toString());
}
public Context getContext() {
return context;
}
public HttpClientRequest getRequest() {
return request;
}
public Map<String, String> getCookieMap() {
return cookieMap;
}
@Override
public void addCookie(String name, String value) {
if (cookieMap == null) {
cookieMap = new HashMap<>();
}
cookieMap.put(name, value);
}
@Override
public void addForm(String name, Object value) {
if (formMap == null) {
formMap = new HashMap<>();
}
if (value != null) {
formMap.put(name, value);
}
}
@Override
public void putHeader(String name, String value) {
request.putHeader(name, value);
}
@Override
public MultiMap getHeaders() {
return request.headers();
}
}