Skip to content

Commit

Permalink
Add transfer encoding to multipart parts.
Browse files Browse the repository at this point in the history
Also:

 * Reject multiple annotations on method parameters.
 * Pre-size StringBuilder instances to their likely size.
  • Loading branch information
JakeWharton committed Oct 3, 2014
1 parent c60d01d commit 5795931
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 76 deletions.
21 changes: 15 additions & 6 deletions retrofit/src/main/java/retrofit/RequestBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.net.URLEncoder;
import java.util.ArrayList;
Expand All @@ -27,6 +28,8 @@
import retrofit.client.Header;
import retrofit.client.Request;
import retrofit.converter.Converter;
import retrofit.http.Part;
import retrofit.http.PartMap;
import retrofit.mime.FormUrlEncodedTypedOutput;
import retrofit.mime.MultipartTypedOutput;
import retrofit.mime.TypedOutput;
Expand All @@ -39,6 +42,7 @@ final class RequestBuilder implements RequestInterceptor.RequestFacade {
private final Converter converter;
private final String[] paramNames;
private final RestMethodInfo.ParamUsage[] paramUsages;
private final Annotation[] paramAnnotations;
private final String requestMethod;
private final boolean isSynchronous;
private final boolean isObservable;
Expand All @@ -59,6 +63,7 @@ final class RequestBuilder implements RequestInterceptor.RequestFacade {

paramNames = methodInfo.requestParamNames;
paramUsages = methodInfo.requestParamUsage;
paramAnnotations = methodInfo.requestParamAnnotation;
requestMethod = methodInfo.requestMethod;
isSynchronous = methodInfo.isSynchronous;
isObservable = methodInfo.isObservable;
Expand Down Expand Up @@ -187,6 +192,7 @@ void setArguments(Object[] args) {
}
for (int i = 0; i < count; i++) {
String name = paramNames[i];
Annotation annotation = paramAnnotations[i];
Object value = args[i];
RestMethodInfo.ParamUsage paramUsage = paramUsages[i];
switch (paramUsage) {
Expand Down Expand Up @@ -275,27 +281,30 @@ void setArguments(Object[] args) {
break;
case PART:
if (value != null) { // Skip null values.
String transferEncoding = ((Part) annotation).encoding();
if (value instanceof TypedOutput) {
multipartBody.addPart(name, (TypedOutput) value);
multipartBody.addPart(name, transferEncoding, (TypedOutput) value);
} else if (value instanceof String) {
multipartBody.addPart(name, new TypedString((String) value));
multipartBody.addPart(name, transferEncoding, new TypedString((String) value));
} else {
multipartBody.addPart(name, converter.toBody(value));
multipartBody.addPart(name, transferEncoding, converter.toBody(value));
}
}
break;
case PART_MAP:
if (value != null) { // Skip null values.
String transferEncoding = ((PartMap) annotation).encoding();
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
String entryName = entry.getKey().toString();
Object entryValue = entry.getValue();
if (entryValue != null) { // Skip null values.
if (entryValue instanceof TypedOutput) {
multipartBody.addPart(entryName, (TypedOutput) entryValue);
multipartBody.addPart(entryName, transferEncoding, (TypedOutput) entryValue);
} else if (entryValue instanceof String) {
multipartBody.addPart(entryName, new TypedString((String) entryValue));
multipartBody.addPart(entryName, transferEncoding,
new TypedString((String) entryValue));
} else {
multipartBody.addPart(entryName, converter.toBody(entryValue));
multipartBody.addPart(entryName, transferEncoding, converter.toBody(entryValue));
}
}
}
Expand Down
132 changes: 74 additions & 58 deletions retrofit/src/main/java/retrofit/RestMethodInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ enum RequestType {
// Parameter-level details
String[] requestParamNames;
ParamUsage[] requestParamUsage;
Annotation[] requestParamAnnotation;

RestMethodInfo(Method method) {
this.method = method;
Expand Down Expand Up @@ -320,115 +321,115 @@ private static Type getParameterUpperBound(ParameterizedType type) {
}

/**
* Loads {@link #requestParamNames} and {@link #requestParamUsage}. Must be called after
* {@link #parseMethodAnnotations()}.
* Loads {@link #requestParamNames}, {@link #requestParamUsage}, and
* {@link #requestParamAnnotation}. Must be called after {@link #parseMethodAnnotations()}.
*/
private void parseParameters() {
Class<?>[] parameterTypes = method.getParameterTypes();
Class<?>[] methodParameterTypes = method.getParameterTypes();

Annotation[][] parameterAnnotationArrays = method.getParameterAnnotations();
int count = parameterAnnotationArrays.length;
Annotation[][] methodParameterAnnotationArrays = method.getParameterAnnotations();
int count = methodParameterAnnotationArrays.length;
if (!isSynchronous && !isObservable) {
count -= 1; // Callback is last argument when not a synchronous method.
}

String[] paramNames = new String[count];
requestParamNames = paramNames;
ParamUsage[] paramUsage = new ParamUsage[count];
requestParamUsage = paramUsage;
String[] requestParamNames = new String[count];
ParamUsage[] requestParamUsage = new ParamUsage[count];
Annotation[] requestParamAnnotation = new Annotation[count];

boolean gotField = false;
boolean gotPart = false;
boolean gotBody = false;

for (int i = 0; i < count; i++) {
Class<?> parameterType = parameterTypes[i];
Annotation[] parameterAnnotations = parameterAnnotationArrays[i];
if (parameterAnnotations != null) {
for (Annotation parameterAnnotation : parameterAnnotations) {
Class<? extends Annotation> annotationType = parameterAnnotation.annotationType();

if (annotationType == Path.class) {
String name = ((Path) parameterAnnotation).value();
Class<?> methodParameterType = methodParameterTypes[i];
Annotation[] methodParameterAnnotations = methodParameterAnnotationArrays[i];
if (methodParameterAnnotations != null) {
for (Annotation methodParameterAnnotation : methodParameterAnnotations) {
Class<? extends Annotation> methodAnnotationType =
methodParameterAnnotation.annotationType();

if (methodAnnotationType == Path.class) {
String name = ((Path) methodParameterAnnotation).value();
validatePathName(i, name);

paramNames[i] = name;
paramUsage[i] = ParamUsage.PATH;
} else if (annotationType == EncodedPath.class) {
String name = ((EncodedPath) parameterAnnotation).value();
requestParamNames[i] = name;
requestParamUsage[i] = ParamUsage.PATH;
} else if (methodAnnotationType == EncodedPath.class) {
String name = ((EncodedPath) methodParameterAnnotation).value();
validatePathName(i, name);

paramNames[i] = name;
paramUsage[i] = ParamUsage.ENCODED_PATH;
} else if (annotationType == Query.class) {
String name = ((Query) parameterAnnotation).value();
requestParamNames[i] = name;
requestParamUsage[i] = ParamUsage.ENCODED_PATH;
} else if (methodAnnotationType == Query.class) {
String name = ((Query) methodParameterAnnotation).value();

paramNames[i] = name;
paramUsage[i] = ParamUsage.QUERY;
} else if (annotationType == EncodedQuery.class) {
String name = ((EncodedQuery) parameterAnnotation).value();
requestParamNames[i] = name;
requestParamUsage[i] = ParamUsage.QUERY;
} else if (methodAnnotationType == EncodedQuery.class) {
String name = ((EncodedQuery) methodParameterAnnotation).value();

paramNames[i] = name;
paramUsage[i] = ParamUsage.ENCODED_QUERY;
} else if (annotationType == QueryMap.class) {
if (!Map.class.isAssignableFrom(parameterType)) {
requestParamNames[i] = name;
requestParamUsage[i] = ParamUsage.ENCODED_QUERY;
} else if (methodAnnotationType == QueryMap.class) {
if (!Map.class.isAssignableFrom(methodParameterType)) {
throw parameterError(i, "@QueryMap parameter type must be Map.");
}

paramUsage[i] = ParamUsage.QUERY_MAP;
} else if (annotationType == EncodedQueryMap.class) {
if (!Map.class.isAssignableFrom(parameterType)) {
requestParamUsage[i] = ParamUsage.QUERY_MAP;
} else if (methodAnnotationType == EncodedQueryMap.class) {
if (!Map.class.isAssignableFrom(methodParameterType)) {
throw parameterError(i, "@EncodedQueryMap parameter type must be Map.");
}

paramUsage[i] = ParamUsage.ENCODED_QUERY_MAP;
} else if (annotationType == Header.class) {
String name = ((Header) parameterAnnotation).value();
requestParamUsage[i] = ParamUsage.ENCODED_QUERY_MAP;
} else if (methodAnnotationType == Header.class) {
String name = ((Header) methodParameterAnnotation).value();

paramNames[i] = name;
paramUsage[i] = ParamUsage.HEADER;
} else if (annotationType == Field.class) {
requestParamNames[i] = name;
requestParamUsage[i] = ParamUsage.HEADER;
} else if (methodAnnotationType == Field.class) {
if (requestType != RequestType.FORM_URL_ENCODED) {
throw parameterError(i, "@Field parameters can only be used with form encoding.");
}

String name = ((Field) parameterAnnotation).value();
String name = ((Field) methodParameterAnnotation).value();

gotField = true;
paramNames[i] = name;
paramUsage[i] = ParamUsage.FIELD;
} else if (annotationType == FieldMap.class) {
requestParamNames[i] = name;
requestParamUsage[i] = ParamUsage.FIELD;
} else if (methodAnnotationType == FieldMap.class) {
if (requestType != RequestType.FORM_URL_ENCODED) {
throw parameterError(i, "@FieldMap parameters can only be used with form encoding.");
}
if (!Map.class.isAssignableFrom(parameterType)) {
if (!Map.class.isAssignableFrom(methodParameterType)) {
throw parameterError(i, "@FieldMap parameter type must be Map.");
}

gotField = true;
paramUsage[i] = ParamUsage.FIELD_MAP;
} else if (annotationType == Part.class) {
requestParamUsage[i] = ParamUsage.FIELD_MAP;
} else if (methodAnnotationType == Part.class) {
if (requestType != RequestType.MULTIPART) {
throw parameterError(i, "@Part parameters can only be used with multipart encoding.");
}

String name = ((Part) parameterAnnotation).value();
String name = ((Part) methodParameterAnnotation).value();

gotPart = true;
paramNames[i] = name;
paramUsage[i] = ParamUsage.PART;
} else if (annotationType == PartMap.class) {
requestParamNames[i] = name;
requestParamUsage[i] = ParamUsage.PART;
} else if (methodAnnotationType == PartMap.class) {
if (requestType != RequestType.MULTIPART) {
throw parameterError(i,
"@PartMap parameters can only be used with multipart encoding.");
}
if (!Map.class.isAssignableFrom(parameterType)) {
if (!Map.class.isAssignableFrom(methodParameterType)) {
throw parameterError(i, "@PartMap parameter type must be Map.");
}

gotPart = true;
paramUsage[i] = ParamUsage.PART_MAP;
} else if (annotationType == Body.class) {
requestParamUsage[i] = ParamUsage.PART_MAP;
} else if (methodAnnotationType == Body.class) {
if (requestType != RequestType.SIMPLE) {
throw parameterError(i,
"@Body parameters cannot be used with form or multi-part encoding.");
Expand All @@ -438,12 +439,23 @@ private void parseParameters() {
}

gotBody = true;
paramUsage[i] = ParamUsage.BODY;
requestParamUsage[i] = ParamUsage.BODY;
} else {
// This is a non-Retrofit annotation. Skip to the next one.
continue;
}

if (requestParamAnnotation[i] != null) {
throw parameterError(i,
"Multiple Retrofit annotations found, only one allowed: @%s, @%s.",
requestParamAnnotation[i].annotationType().getSimpleName(),
methodAnnotationType.getSimpleName());
}
requestParamAnnotation[i] = methodParameterAnnotation;
}
}

if (paramUsage[i] == null) {
if (requestParamUsage[i] == null) {
throw parameterError(i, "No Retrofit annotation found.");
}
}
Expand All @@ -457,6 +469,10 @@ private void parseParameters() {
if (requestType == RequestType.MULTIPART && !gotPart) {
throw methodError("Multipart method must contain at least one @Part.");
}

this.requestParamNames = requestParamNames;
this.requestParamUsage = requestParamUsage;
this.requestParamAnnotation = requestParamAnnotation;
}

private void validatePathName(int index, String name) {
Expand Down
3 changes: 3 additions & 0 deletions retrofit/src/main/java/retrofit/http/Part.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static retrofit.mime.MultipartTypedOutput.DEFAULT_TRANSFER_ENCODING;

/**
* Denotes a single part of a multi-part request.
Expand Down Expand Up @@ -53,4 +54,6 @@
@Retention(RUNTIME)
public @interface Part {
String value();
/** The {@code Content-Transfer-Encoding} of this part. */
String encoding() default DEFAULT_TRANSFER_ENCODING;
}
3 changes: 3 additions & 0 deletions retrofit/src/main/java/retrofit/http/PartMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static retrofit.mime.MultipartTypedOutput.DEFAULT_TRANSFER_ENCODING;

/**
* Denotes name and value parts of a multi-part request
Expand Down Expand Up @@ -49,4 +50,6 @@
@Target(PARAMETER)
@Retention(RUNTIME)
public @interface PartMap {
/** The {@code Content-Transfer-Encoding} of this part. */
String encoding() default DEFAULT_TRANSFER_ENCODING;
}
Loading

0 comments on commit 5795931

Please sign in to comment.