Repository: square/retrofit
Issue: XML api sample

Would be nice to be able to easily use retrofit for XML apis like AWS.  I'll volunteer to make a sample, if you don't mind giving me pointers as to what might be needed in abstract.


Issue comments:
Is there a standard for object to XML mapping? GSON and Jackson are the canonical JSON mappers, protocol buffers are protocol buffers, etc.

Most common is jaxb.  It it is a wreck for generic types.  Ex. it forces you to make classes like `Things` as `List<Thing>` isn't possible at top-level.   Jackson 2+ has [limited XML support](http://www.cowtowncoder.com/blog/archives/2012/03/entry_467.html).

Personally, I tend to bite the bullet and write SAX often enough as the shape of XML is often far from ideal forms of java objects, but usually not terribly nested.  I concede being weird here.

Once issues #225 and #223 are merged, I'll write an example S3 api.

I think I'll go ahead and work on this now, as it should help make things I've failed to explain earlier more concrete.

closing this PR, as I don't want to distract important change for the sake of optional change.


Pull request review comments:
probably need addQueryParam, unless addPathParam implies this.  (ex. many signatures are query parameters)

I'm ok leaving this out, but affecting the body of the request often comes up in crappier apis (like SOAP).  If we leave this out, I'd need to do it in a custom http impl I think.

I had query in my initial proposal for this. Left it out since it seemed like a strange thing. I don't mind having it for consistency.

Yes, please!

Thanks, @JakeWharton.  The use case is temporarily signed Urls have a query
param designating the signature.

For example: 
GET /photos/puppy.jpg?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&
    Signature=NpgCjnDzrM%2BWFzoENXmpNDUsSn8%3D&
    Expires=1175139620

What is the signature in that case? Does it require having access to the non-query URL or request body?

Depends oauth, s3, etc have different signing practices.  Many need to read
the entire request.  Some include an md5 header of the body.

For example, s3 needs to read the request method (PUT and GET are very
important to differentiate from a signature pov)

This definitely isn't designed with signing in mind since we provide no access to the underlying request. It's something to consider moving forward, though.


Patch that resolved the issue:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ea6cde500..91e269fb94 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,14 @@
 Change Log
 ==========
 
+Version 1.1.0 *(In Development)*
+--------------------------------
+
+ * Introduce `RequestInterceptor` to replace `RequestHeaders`. An interceptor provided to the
+   `RestAdapter.Builder` will be called for every request and allow setting both headers and
+   additional path parameter replacements.
+
+
 Version 1.0.2 *(2013-05-23)*
 ----------------------------
 
diff --git a/pom.xml b/pom.xml
index 1ac4386273..19dc9a93db 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,7 +25,7 @@
 
   <groupId>com.squareup.retrofit</groupId>
   <artifactId>parent</artifactId>
-  <version>1.0.3-SNAPSHOT</version>
+  <version>1.1.0-SNAPSHOT</version>
   <packaging>pom</packaging>
 
   <name>Retrofit (Parent)</name>
diff --git a/retrofit-samples/github-client/pom.xml b/retrofit-samples/github-client/pom.xml
index e3653123b3..1ba5ee2d6a 100644
--- a/retrofit-samples/github-client/pom.xml
+++ b/retrofit-samples/github-client/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.retrofit.samples</groupId>
     <artifactId>parent</artifactId>
-    <version>1.0.3-SNAPSHOT</version>
+    <version>1.1.0-SNAPSHOT</version>
     <relativePath>../pom.xml</relativePath>
   </parent>
 
diff --git a/retrofit-samples/pom.xml b/retrofit-samples/pom.xml
index 682ce75183..622b520634 100644
--- a/retrofit-samples/pom.xml
+++ b/retrofit-samples/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.retrofit</groupId>
     <artifactId>parent</artifactId>
-    <version>1.0.3-SNAPSHOT</version>
+    <version>1.1.0-SNAPSHOT</version>
     <relativePath>../pom.xml</relativePath>
   </parent>
 
diff --git a/retrofit/pom.xml b/retrofit/pom.xml
index c7bfc0c9c0..8abc2eb4cf 100644
--- a/retrofit/pom.xml
+++ b/retrofit/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.retrofit</groupId>
     <artifactId>parent</artifactId>
-    <version>1.0.3-SNAPSHOT</version>
+    <version>1.1.0-SNAPSHOT</version>
     <relativePath>../pom.xml</relativePath>
   </parent>
 
diff --git a/retrofit/src/main/java/retrofit/RequestBuilder.java b/retrofit/src/main/java/retrofit/RequestBuilder.java
index dd69c9add0..8847422379 100644
--- a/retrofit/src/main/java/retrofit/RequestBuilder.java
+++ b/retrofit/src/main/java/retrofit/RequestBuilder.java
@@ -26,180 +26,185 @@
 import retrofit.mime.MultipartTypedOutput;
 import retrofit.mime.TypedOutput;
 
-/** Builds HTTP requests from Java method invocations. */
-final class RequestBuilder {
+final class RequestBuilder implements RequestInterceptor.RequestFacade {
   private final Converter converter;
-
-  private RestMethodInfo methodInfo;
-  private Object[] args;
+  private final List<Header> headers;
+  private final StringBuilder queryParams;
+  private final String[] paramNames;
+  private final RestMethodInfo.ParamUsage[] paramUsages;
+  private final String requestMethod;
+  private final boolean isSynchronous;
+
+  private final FormUrlEncodedTypedOutput formBody;
+  private final MultipartTypedOutput multipartBody;
+  private TypedOutput body;
+
+  private String relativeUrl;
   private String apiUrl;
-  private List<retrofit.client.Header> headers;
 
-  RequestBuilder(Converter converter) {
+  RequestBuilder(Converter converter, RestMethodInfo methodInfo) {
     this.converter = converter;
-  }
 
-  /** Supply cached method metadata info. */
-  RequestBuilder methodInfo(RestMethodInfo methodDetails) {
-    this.methodInfo = methodDetails;
-    return this;
-  }
+    paramNames = methodInfo.requestParamNames;
+    paramUsages = methodInfo.requestParamUsage;
+    requestMethod = methodInfo.requestMethod;
+    isSynchronous = methodInfo.isSynchronous;
 
-  /** Base API url. */
-  RequestBuilder apiUrl(String apiUrl) {
-    this.apiUrl = apiUrl;
-    return this;
-  }
-
-  /** Arguments from method invocation. */
-  RequestBuilder args(Object[] args) {
-    this.args = args;
-    return this;
-  }
+    headers = new ArrayList<Header>();
+    queryParams = new StringBuilder();
 
-  /** A list of custom headers. */
-  RequestBuilder headers(List<retrofit.client.Header> headers) {
-    this.headers = headers;
-    return this;
-  }
+    relativeUrl = methodInfo.requestUrl;
 
-  /**
-   * Construct a {@link Request} from the supplied information. You <strong>must</strong> call
-   * {@link #methodInfo}, {@link #apiUrl}, {@link #args}, and {@link #headers} before invoking this
-   * method.
-   */
-  Request build() throws UnsupportedEncodingException {
-    String apiUrl = this.apiUrl;
+    String requestQuery = methodInfo.requestQuery;
+    if (requestQuery != null) {
+      queryParams.append('?').append(requestQuery);
+    }
 
-    StringBuilder url = new StringBuilder(apiUrl);
-    if (apiUrl.endsWith("/")) {
-      // We require relative paths to start with '/'. Prevent a double-slash.
-      url.deleteCharAt(url.length() - 1);
+    switch (methodInfo.requestType) {
+      case FORM_URL_ENCODED:
+        formBody = new FormUrlEncodedTypedOutput();
+        multipartBody = null;
+        body = formBody;
+        break;
+      case MULTIPART:
+        formBody = null;
+        multipartBody = new MultipartTypedOutput();
+        body = multipartBody;
+        break;
+      case SIMPLE:
+        formBody = null;
+        multipartBody = null;
+        // If present, 'body' will be set in 'setArguments' call.
+        break;
+      default:
+        throw new IllegalArgumentException("Unknown request type: " + methodInfo.requestType);
     }
+  }
 
-    // Append the method relative URL.
-    url.append(buildRelativeUrl());
+  void setApiUrl(String apiUrl) {
+    this.apiUrl = apiUrl;
+  }
 
-    // Append query parameters, if needed.
-    if (methodInfo.hasQueryParams) {
-      boolean first = true;
-      String requestQuery = methodInfo.requestQuery;
-      if (requestQuery != null) {
-        url.append('?').append(requestQuery);
-        first = false;
-      }
-      String[] requestQueryName = methodInfo.requestQueryName;
-      for (int i = 0; i < requestQueryName.length; i++) {
-        String query = requestQueryName[i];
-        if (query != null) {
-          Object arg = args[i];
-          if (arg != null) { // Null values are skipped.
-            String value = URLEncoder.encode(String.valueOf(arg), "UTF-8");
-            url.append(first ? '?' : '&').append(query).append('=').append(value);
-            first = false;
-          }
-        }
-      }
+  @Override public void addHeader(String name, String value) {
+    if (name == null) {
+      throw new IllegalArgumentException("Header name must not be null.");
     }
+    headers.add(new Header(name, value));
+  }
 
-    List<retrofit.client.Header> headers = new ArrayList<retrofit.client.Header>();
-    if (this.headers != null) {
-      headers.addAll(this.headers);
+  @Override public void addPathParam(String name, String value) {
+    if (name == null) {
+      throw new IllegalArgumentException("Path replacement name must not be null.");
     }
-    List<Header> methodHeaders = methodInfo.headers;
-    if (methodHeaders != null) {
-      headers.addAll(methodHeaders);
+    if (value == null) {
+      throw new IllegalArgumentException(
+          "Path replacement \"" + name + "\" value must not be null.");
     }
-    // RFC 2616: Header names are case-insensitive.
-    String[] requestParamHeader = methodInfo.requestParamHeader;
-    if (requestParamHeader != null) {
-      for (int i = 0; i < requestParamHeader.length; i++) {
-        String name = requestParamHeader[i];
-        if (name == null) continue;
-        Object arg = args[i];
-        if (arg != null) {
-          headers.add(new retrofit.client.Header(name, String.valueOf(arg)));
-        }
-      }
+    try {
+      String encodedValue = URLEncoder.encode(String.valueOf(value), "UTF-8");
+      relativeUrl = relativeUrl.replace("{" + name + "}", encodedValue);
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException(
+          "Unable to convert path parameter \"" + name + "\" value to UTF-8:" + value, e);
     }
+  }
 
-    return new Request(methodInfo.requestMethod, url.toString(), headers, buildBody());
+  private void addQueryParam(String name, String value) {
+    if (name == null) {
+      throw new IllegalArgumentException("Query param name must not be null.");
+    }
+    if (value == null) {
+      throw new IllegalArgumentException("Query param \"" + name + "\" value must not be null.");
+    }
+    try {
+      value = URLEncoder.encode(String.valueOf(value), "UTF-8");
+      StringBuilder queryParams = this.queryParams;
+      queryParams.append(queryParams.length() > 0 ? '&' : '?');
+      queryParams.append(name).append('=').append(value);
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException(
+          "Unable to convert query parameter \"" + name + "\" value to UTF-8: " + value, e);
+    }
   }
 
-  /** Create the final relative URL by performing parameter replacement. */
-  private String buildRelativeUrl() throws UnsupportedEncodingException {
-    String replacedPath = methodInfo.requestUrl;
-    String[] requestUrlParam = methodInfo.requestUrlParam;
-    for (int i = 0; i < requestUrlParam.length; i++) {
-      String param = requestUrlParam[i];
-      if (param != null) {
-        Object arg = args[i];
-        if (arg == null) {
-          throw new IllegalArgumentException("Path parameters must not be null: " + param + ".");
-        }
-        String value = URLEncoder.encode(String.valueOf(arg), "UTF-8");
-        replacedPath = replacedPath.replace("{" + param + "}", value);
+  void setArguments(Object[] args) {
+    if (args == null) {
+      return;
+    }
+    int count = args.length;
+    if (!isSynchronous) {
+      count -= 1;
+    }
+    for (int i = 0; i < count; i++) {
+      String name = paramNames[i];
+      Object value = args[i];
+      RestMethodInfo.ParamUsage paramUsage = paramUsages[i];
+      switch (paramUsage) {
+        case PATH:
+          if (value == null) {
+            throw new IllegalArgumentException(
+                "Path parameter \"" + name + "\" value must not be null.");
+          }
+          addPathParam(name, value.toString());
+          break;
+        case QUERY:
+          if (value != null) { // Skip null values.
+            addQueryParam(name, value.toString());
+          }
+          break;
+        case HEADER:
+          if (value != null) { // Skip null values.
+            addHeader(name, value.toString());
+          }
+          break;
+        case FIELD:
+          if (value != null) { // Skip null values.
+            formBody.addField(name, value.toString());
+          }
+          break;
+        case PART:
+          if (value == null) {
+            throw new IllegalArgumentException(
+                "Multipart part \"" + name + "\" value must not be null.");
+          }
+          if (value instanceof TypedOutput) {
+            multipartBody.addPart(name, (TypedOutput) value);
+          } else {
+            multipartBody.addPart(name, converter.toBody(value));
+          }
+          break;
+        case BODY:
+          if (value == null) {
+            throw new IllegalArgumentException("Body parameter value must not be null.");
+          }
+          if (value instanceof TypedOutput) {
+            body = (TypedOutput) value;
+          } else {
+            body = converter.toBody(value);
+          }
+          break;
+        default:
+          throw new IllegalArgumentException("Unknown parameter usage: " + paramUsage);
       }
     }
-    return replacedPath;
   }
 
-  /** Create the request body using the method info and invocation arguments. */
-  private TypedOutput buildBody() {
-    switch (methodInfo.requestType) {
-      case SIMPLE: {
-        int bodyIndex = methodInfo.bodyIndex;
-        if (bodyIndex == RestMethodInfo.NO_BODY) {
-          return null;
-        }
-        Object body = args[bodyIndex];
-        if (body == null) {
-          throw new IllegalArgumentException("Body must not be null.");
-        }
-        if (body instanceof TypedOutput) {
-          return (TypedOutput) body;
-        } else {
-          return converter.toBody(body);
-        }
-      }
+  Request build() throws UnsupportedEncodingException {
+    String apiUrl = this.apiUrl;
 
-      case FORM_URL_ENCODED: {
-        FormUrlEncodedTypedOutput body = new FormUrlEncodedTypedOutput();
-        String[] requestFormFields = methodInfo.requestFormFields;
-        for (int i = 0; i < requestFormFields.length; i++) {
-          String name = requestFormFields[i];
-          if (name != null) {
-            Object value = args[i];
-            if (value != null) { // Null values are skipped.
-              body.addField(name, String.valueOf(value));
-            }
-          }
-        }
-        return body;
-      }
+    StringBuilder url = new StringBuilder(apiUrl);
+    if (apiUrl.endsWith("/")) {
+      // We require relative paths to start with '/'. Prevent a double-slash.
+      url.deleteCharAt(url.length() - 1);
+    }
 
-      case MULTIPART: {
-        MultipartTypedOutput body = new MultipartTypedOutput();
-        String[] requestMultipartPart = methodInfo.requestMultipartPart;
-        for (int i = 0; i < requestMultipartPart.length; i++) {
-          String name = requestMultipartPart[i];
-          if (name != null) {
-            Object value = args[i];
-            if (value == null) {
-              throw new IllegalArgumentException("Multipart part must not be null: " + name + ".");
-            }
-            if (value instanceof TypedOutput) {
-              body.addPart(name, (TypedOutput) value);
-            } else {
-              body.addPart(name, converter.toBody(value));
-            }
-          }
-        }
-        return body;
-      }
+    url.append(relativeUrl);
 
-      default:
-        throw new IllegalArgumentException("Unknown request type " + methodInfo.requestType);
+    StringBuilder queryParams = this.queryParams;
+    if (queryParams.length() > 0) {
+      url.append(queryParams);
     }
+
+    return new Request(requestMethod, url.toString(), headers, body);
   }
 }
diff --git a/retrofit/src/main/java/retrofit/RequestHeaders.java b/retrofit/src/main/java/retrofit/RequestHeaders.java
deleted file mode 100644
index c1bef05d5e..0000000000
--- a/retrofit/src/main/java/retrofit/RequestHeaders.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed 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 retrofit;
-
-import java.util.Collections;
-import java.util.List;
-import retrofit.client.Header;
-
-/** Manages headers for each request. */
-public interface RequestHeaders {
-  /**
-   * Get a list of headers for a request. This method will be called once for each request allowing
-   * you to change the list as the state of your application changes.
-   */
-  List<Header> get();
-
-  /** Empty header list. */
-  RequestHeaders NONE = new RequestHeaders() {
-    @Override public List<Header> get() {
-      return Collections.emptyList();
-    }
-  };
-}
diff --git a/retrofit/src/main/java/retrofit/RequestInterceptor.java b/retrofit/src/main/java/retrofit/RequestInterceptor.java
new file mode 100644
index 0000000000..d5b1d09538
--- /dev/null
+++ b/retrofit/src/main/java/retrofit/RequestInterceptor.java
@@ -0,0 +1,25 @@
+package retrofit;
+
+/** Intercept every request before it is executed in order to add additional data. */
+public interface RequestInterceptor {
+  /** Called for every request. Add data using methods on the supplied {@link RequestFacade}. */
+  void intercept(RequestFacade request);
+
+  interface RequestFacade {
+    /** Add a header to the request. This will not replace any existing headers. */
+    void addHeader(String name, String value);
+
+    /**
+     * Add a path parameter replacement. This works exactly like a {@link retrofit.http.Part
+     * &#64;Part}-annotated method argument.
+     */
+    void addPathParam(String name, String value);
+  }
+
+  /** A {@link RequestInterceptor} which does no modification of requests. */
+  RequestInterceptor NONE = new RequestInterceptor() {
+    @Override public void intercept(RequestFacade request) {
+      // Do nothing.
+    }
+  };
+}
diff --git a/retrofit/src/main/java/retrofit/RestAdapter.java b/retrofit/src/main/java/retrofit/RestAdapter.java
index 8b549fc760..a447a0fc22 100644
--- a/retrofit/src/main/java/retrofit/RestAdapter.java
+++ b/retrofit/src/main/java/retrofit/RestAdapter.java
@@ -117,7 +117,7 @@ public interface Log {
   private final Client.Provider clientProvider;
   private final Executor httpExecutor;
   private final Executor callbackExecutor;
-  private final RequestHeaders requestHeaders;
+  private final RequestInterceptor requestInterceptor;
   private final Converter converter;
   private final Profiler profiler;
   private final ErrorHandler errorHandler;
@@ -125,13 +125,13 @@ public interface Log {
   private volatile boolean debug;
 
   private RestAdapter(Server server, Client.Provider clientProvider, Executor httpExecutor,
-      Executor callbackExecutor, RequestHeaders requestHeaders, Converter converter,
+      Executor callbackExecutor, RequestInterceptor requestInterceptor, Converter converter,
       Profiler profiler, ErrorHandler errorHandler, Log log, boolean debug) {
     this.server = server;
     this.clientProvider = clientProvider;
     this.httpExecutor = httpExecutor;
     this.callbackExecutor = callbackExecutor;
-    this.requestHeaders = requestHeaders;
+    this.requestInterceptor = requestInterceptor;
     this.converter = converter;
     this.profiler = profiler;
     this.errorHandler = errorHandler;
@@ -214,12 +214,13 @@ private Object invokeRequest(RestMethodInfo methodDetails, Object[] args) {
       String serverUrl = server.getUrl();
       String url = serverUrl; // Keep some url in case RequestBuilder throws an exception.
       try {
-        Request request = new RequestBuilder(converter) //
-            .apiUrl(serverUrl) //
-            .args(args) //
-            .headers(requestHeaders.get()) //
-            .methodInfo(methodDetails) //
-            .build();
+        RequestBuilder requestBuilder = new RequestBuilder(converter, methodDetails);
+        requestBuilder.setApiUrl(serverUrl);
+        requestBuilder.setArguments(args);
+
+        requestInterceptor.intercept(requestBuilder);
+
+        Request request = requestBuilder.build();
         url = request.getUrl();
 
         if (!methodDetails.isSynchronous) {
@@ -408,7 +409,7 @@ public static class Builder {
     private Client.Provider clientProvider;
     private Executor httpExecutor;
     private Executor callbackExecutor;
-    private RequestHeaders requestHeaders;
+    private RequestInterceptor requestInterceptor;
     private Converter converter;
     private Profiler profiler;
     private ErrorHandler errorHandler;
@@ -473,12 +474,12 @@ public Builder setExecutors(Executor httpExecutor, Executor callbackExecutor) {
       return this;
     }
 
-    /**  */
-    public Builder setRequestHeaders(RequestHeaders requestHeaders) {
-      if (requestHeaders == null) {
-        throw new NullPointerException("Request headers may not be null.");
+    /** A request interceptor for adding data to every request. */
+    public Builder setRequestInterceptor(RequestInterceptor requestInterceptor) {
+      if (requestInterceptor == null) {
+        throw new NullPointerException("Request interceptor may not be null.");
       }
-      this.requestHeaders = requestHeaders;
+      this.requestInterceptor = requestInterceptor;
       return this;
     }
 
@@ -533,8 +534,8 @@ public RestAdapter build() {
         throw new IllegalArgumentException("Server may not be null.");
       }
       ensureSaneDefaults();
-      return new RestAdapter(server, clientProvider, httpExecutor, callbackExecutor, requestHeaders,
-          converter, profiler, errorHandler, log, debug);
+      return new RestAdapter(server, clientProvider, httpExecutor, callbackExecutor,
+          requestInterceptor, converter, profiler, errorHandler, log, debug);
     }
 
     private void ensureSaneDefaults() {
@@ -556,8 +557,8 @@ private void ensureSaneDefaults() {
       if (log == null) {
         log = Platform.get().defaultLog();
       }
-      if (requestHeaders == null) {
-        requestHeaders = RequestHeaders.NONE;
+      if (requestInterceptor == null) {
+        requestInterceptor = RequestInterceptor.NONE;
       }
     }
   }
diff --git a/retrofit/src/main/java/retrofit/RestMethodInfo.java b/retrofit/src/main/java/retrofit/RestMethodInfo.java
index dfdeffd210..809f6aac44 100644
--- a/retrofit/src/main/java/retrofit/RestMethodInfo.java
+++ b/retrofit/src/main/java/retrofit/RestMethodInfo.java
@@ -39,13 +39,15 @@
 
 /** Request metadata about a service interface declaration. */
 final class RestMethodInfo {
-  static final int NO_BODY = -1;
-
   // Upper and lower characters, digits, underscores, and hyphens, starting with a character.
   private static final String PARAM = "[a-zA-Z][a-zA-Z0-9_-]*";
   private static final Pattern PARAM_NAME_REGEX = Pattern.compile(PARAM);
   private static final Pattern PARAM_URL_REGEX = Pattern.compile("\\{(" + PARAM + ")\\}");
 
+  enum ParamUsage {
+    PATH, QUERY, FIELD, PART, BODY, HEADER
+  }
+
   enum RequestType {
     /** No content-specific logic required. */
     SIMPLE,
@@ -71,13 +73,8 @@ enum RequestType {
   List<retrofit.client.Header> headers;
 
   // Parameter-level details
-  String[] requestUrlParam;
-  String[] requestQueryName;
-  boolean hasQueryParams = false;
-  String[] requestFormFields;
-  String[] requestMultipartPart;
-  String[] requestParamHeader;
-  int bodyIndex = NO_BODY;
+  String[] requestParamNames;
+  ParamUsage[] requestParamUsage;
 
   RestMethodInfo(Method method) {
     this.method = method;
@@ -189,7 +186,6 @@ private void parsePath(String path) {
     if (question != -1 && question < path.length() - 1) {
       url = path.substring(0, question);
       query = path.substring(question + 1);
-      hasQueryParams = true;
 
       // Ensure the query string does not have any named parameters.
       Matcher queryParamMatcher = PARAM_URL_REGEX.matcher(query);
@@ -282,8 +278,7 @@ private boolean parseResponseType() {
   }
 
   /**
-   * Loads {@link #requestUrlParam}, {@link #requestQueryName}, {@link #requestFormFields},
-   * {@link #requestMultipartPart}, and {@link #requestParamHeader}. Must be called after
+   * Loads {@link #requestParamNames} and {@link #requestParamUsage}. Must be called after
    * {@link #parseMethodAnnotations()}.
    */
   private void parseParameters() {
@@ -295,17 +290,16 @@ private void parseParameters() {
       count -= 1; // Callback is last argument when not a synchronous method.
     }
 
-    String[] urlParam = new String[count];
-    String[] queryName = new String[count];
-    String[] formValue = new String[count];
-    String[] multipartPart = new String[count];
-    String[] paramHeader = new String[count];
+    String[] paramNames = new String[count];
+    requestParamNames = paramNames;
+    ParamUsage[] paramUsage = new ParamUsage[count];
+    requestParamUsage = paramUsage;
+
     boolean gotField = false;
     boolean gotPart = false;
+    boolean gotBody = false;
 
     for (int i = 0; i < count; i++) {
-      boolean hasRetrofitAnnotation = false;
-
       Class<?> parameterType = parameterTypes[i];
       Annotation[] parameterAnnotations = parameterAnnotationArrays[i];
       if (parameterAnnotations != null) {
@@ -313,7 +307,6 @@ private void parseParameters() {
           Class<? extends Annotation> annotationType = parameterAnnotation.annotationType();
 
           if (annotationType == Path.class) {
-            hasRetrofitAnnotation = true;
             String name = ((Path) parameterAnnotation).value();
 
             if (!PARAM_NAME_REGEX.matcher(name).matches()) {
@@ -328,21 +321,21 @@ private void parseParameters() {
                   "Method URL \"" + requestUrl + "\" does not contain {" + name + "}.");
             }
 
-            urlParam[i] = name;
+            paramNames[i] = name;
+            paramUsage[i] = ParamUsage.PATH;
           } else if (annotationType == Query.class) {
-            hasRetrofitAnnotation = true;
-            hasQueryParams = true;
             String name = ((Query) parameterAnnotation).value();
 
-            queryName[i] = name;
+            paramNames[i] = name;
+            paramUsage[i] = ParamUsage.QUERY;
           } else if (annotationType == Header.class) {
             String name = ((Header) parameterAnnotation).value();
             if (parameterType != String.class) {
               throw new IllegalStateException("@Header parameter type must be String: " + name);
             }
 
-            hasRetrofitAnnotation = true;
-            paramHeader[i] = name;
+            paramNames[i] = name;
+            paramUsage[i] = ParamUsage.HEADER;
           } else if (annotationType == Field.class) {
             if (requestType != RequestType.FORM_URL_ENCODED) {
               throw new IllegalStateException(
@@ -352,8 +345,8 @@ private void parseParameters() {
             String name = ((Field) parameterAnnotation).value();
 
             gotField = true;
-            hasRetrofitAnnotation = true;
-            formValue[i] = name;
+            paramNames[i] = name;
+            paramUsage[i] = ParamUsage.FIELD;
           } else if (annotationType == Part.class) {
             if (requestType != RequestType.MULTIPART) {
               throw new IllegalStateException(
@@ -363,31 +356,31 @@ private void parseParameters() {
             String name = ((Part) parameterAnnotation).value();
 
             gotPart = true;
-            hasRetrofitAnnotation = true;
-            multipartPart[i] = name;
+            paramNames[i] = name;
+            paramUsage[i] = ParamUsage.PART;
           } else if (annotationType == Body.class) {
             if (requestType != RequestType.SIMPLE) {
               throw new IllegalStateException(
                   "@Body parameters cannot be used with form or multi-part encoding.");
             }
-            if (bodyIndex != NO_BODY) {
+            if (gotBody) {
               throw new IllegalStateException(
                   "Method annotated with multiple Body method annotations: " + method);
             }
 
-            hasRetrofitAnnotation = true;
-            bodyIndex = i;
+            gotBody = true;
+            paramUsage[i] = ParamUsage.BODY;
           }
         }
       }
 
-      if (!hasRetrofitAnnotation) {
+      if (paramUsage[i] == null) {
         throw new IllegalStateException(
-            "No annotations found on parameter " + (i + 1) + " of " + method.getName());
+            "No Retrofit annotation found on parameter " + (i + 1) + " of " + method.getName());
       }
     }
 
-    if (requestType == RequestType.SIMPLE && !requestHasBody && bodyIndex != NO_BODY) {
+    if (requestType == RequestType.SIMPLE && !requestHasBody && gotBody) {
       throw new IllegalStateException("Non-body HTTP method cannot contain @Body or @TypedOutput.");
     }
     if (requestType == RequestType.FORM_URL_ENCODED && !gotField) {
@@ -396,12 +389,6 @@ private void parseParameters() {
     if (requestType == RequestType.MULTIPART && !gotPart) {
       throw new IllegalStateException("Multipart method must contain at least one @Part.");
     }
-
-    requestUrlParam = urlParam;
-    requestQueryName = queryName;
-    requestFormFields = formValue;
-    requestMultipartPart = multipartPart;
-    requestParamHeader = paramHeader;
   }
 
   /**


Test patch:
diff --git a/retrofit/src/test/java/retrofit/RequestBuilderTest.java b/retrofit/src/test/java/retrofit/RequestBuilderTest.java
index 0f0b207569..11fe09615d 100644
--- a/retrofit/src/test/java/retrofit/RequestBuilderTest.java
+++ b/retrofit/src/test/java/retrofit/RequestBuilderTest.java
@@ -21,7 +21,13 @@
 
 import static org.fest.assertions.api.Assertions.assertThat;
 import static org.fest.assertions.api.Assertions.fail;
-import static retrofit.RestMethodInfo.NO_BODY;
+import static retrofit.RestMethodInfo.ParamUsage;
+import static retrofit.RestMethodInfo.ParamUsage.BODY;
+import static retrofit.RestMethodInfo.ParamUsage.FIELD;
+import static retrofit.RestMethodInfo.ParamUsage.HEADER;
+import static retrofit.RestMethodInfo.ParamUsage.PART;
+import static retrofit.RestMethodInfo.ParamUsage.PATH;
+import static retrofit.RestMethodInfo.ParamUsage.QUERY;
 import static retrofit.RestMethodInfo.RequestType;
 
 public class RequestBuilderTest {
@@ -60,7 +66,7 @@ public class RequestBuilderTest {
           .build();
       fail("Null path parameters not allowed.");
     } catch (IllegalArgumentException e) {
-      assertThat(e.getMessage()).isEqualTo("Path parameters must not be null: ping.");
+      assertThat(e.getMessage()).isEqualTo("Path parameter \"ping\" value must not be null.");
     }
   }
 
@@ -78,12 +84,12 @@ public class RequestBuilderTest {
   }
 
   @Test public void queryParamOptional() throws Exception {
-      Request request1 = new Helper() //
-          .setMethod("GET") //
-          .setUrl("http://example.com") //
-          .setPath("/foo/bar/") //
-          .addQueryParam("ping", null) //
-          .build();
+    Request request1 = new Helper() //
+        .setMethod("GET") //
+        .setUrl("http://example.com") //
+        .setPath("/foo/bar/") //
+        .addQueryParam("ping", null) //
+        .build();
     assertThat(request1.getUrl()).isEqualTo("http://example.com/foo/bar/");
 
     Request request2 = new Helper() //
@@ -233,7 +239,7 @@ public class RequestBuilderTest {
           .build();
       fail("Null body not allowed.");
     } catch (IllegalArgumentException e) {
-      assertThat(e.getMessage()).isEqualTo("Body must not be null.");
+      assertThat(e.getMessage()).isEqualTo("Body parameter value must not be null.");
     }
   }
 
@@ -292,7 +298,7 @@ public class RequestBuilderTest {
           .build();
       fail("Null multipart part is not allowed.");
     } catch (IllegalArgumentException e) {
-      assertThat(e.getMessage()).isEqualTo("Multipart part must not be null: ping.");
+      assertThat(e.getMessage()).isEqualTo("Multipart part \"ping\" value must not be null.");
     }
   }
 
@@ -318,7 +324,7 @@ public class RequestBuilderTest {
         .setFormEncoded() //
         .addField("foo", "bar") //
         .addField("ping", null) //
-        .addField("kit", "kat")
+        .addField("kit", "kat") //
         .build();
     assertTypedBytes(request.getBody(), "foo=bar&kit=kat");
   }
@@ -390,17 +396,12 @@ private static class Helper {
     private RequestType requestType = RequestType.SIMPLE;
     private String method;
     private boolean hasBody = false;
-    private boolean hasQueryParams = false;
     private String path;
     private String query;
-    private final List<String> pathParams = new ArrayList<String>();
-    private final List<String> queryParams = new ArrayList<String>();
-    private final List<String> fieldParams = new ArrayList<String>();
-    private final List<String> partParams = new ArrayList<String>();
-    private final List<String> headerParams = new ArrayList<String>();
+    private final List<String> paramNames = new ArrayList<String>();
+    private final List<ParamUsage> paramUsages = new ArrayList<ParamUsage>();
     private final List<Object> args = new ArrayList<Object>();
     private final List<Header> headers = new ArrayList<Header>();
-    private int bodyIndex = NO_BODY;
     private String url;
 
     Helper setMethod(String method) {
@@ -425,49 +426,48 @@ Helper setPath(String path) {
 
     Helper setQuery(String query) {
       this.query = query;
-      hasQueryParams = true;
       return this;
     }
 
-    private void addParam(String path, String query, String field, String part, String header,
-        Object value) {
-      pathParams.add(path);
-      queryParams.add(query);
-      fieldParams.add(field);
-      partParams.add(part);
-      headerParams.add(header);
-      args.add(value);
-    }
-
     Helper addPathParam(String name, Object value) {
-      addParam(name, null, null, null, null, value);
+      paramNames.add(name);
+      paramUsages.add(PATH);
+      args.add(value);
       return this;
     }
 
     Helper addQueryParam(String name, String value) {
-      addParam(null, name, null, null, null, value);
-      hasQueryParams = true;
+      paramNames.add(name);
+      paramUsages.add(QUERY);
+      args.add(value);
       return this;
     }
 
     Helper addField(String name, String value) {
-      addParam(null, null, name, null, null, value);
+      paramNames.add(name);
+      paramUsages.add(FIELD);
+      args.add(value);
       return this;
     }
 
     Helper addPart(String name, Object value) {
-      addParam(null, null, null, name, null, value);
+      paramNames.add(name);
+      paramUsages.add(PART);
+      args.add(value);
       return this;
     }
 
     Helper setBody(Object value) {
-      addParam(null, null, null, null, null, value);
-      bodyIndex = args.size() - 1;
+      paramNames.add(null);
+      paramUsages.add(BODY);
+      args.add(value);
       return this;
     }
 
     Helper addHeaderParam(String name, Object value) {
-      addParam(null, null, null, null, name, value);
+      paramNames.add(name);
+      paramUsages.add(HEADER);
+      args.add(value);
       return this;
     }
 
@@ -503,21 +503,20 @@ Request build() throws Exception {
       methodInfo.requestUrl = path;
       methodInfo.requestUrlParamNames = RestMethodInfo.parsePathParameters(path);
       methodInfo.requestQuery = query;
-      methodInfo.hasQueryParams = hasQueryParams;
-      methodInfo.requestUrlParam = pathParams.toArray(new String[pathParams.size()]);
-      methodInfo.requestQueryName = queryParams.toArray(new String[queryParams.size()]);
-      methodInfo.requestFormFields = fieldParams.toArray(new String[fieldParams.size()]);
-      methodInfo.requestMultipartPart = partParams.toArray(new String[partParams.size()]);
-      methodInfo.requestParamHeader = headerParams.toArray(new String[headerParams.size()]);
-      methodInfo.bodyIndex = bodyIndex;
+      methodInfo.requestParamNames = paramNames.toArray(new String[paramNames.size()]);
+      methodInfo.requestParamUsage = paramUsages.toArray(new ParamUsage[paramUsages.size()]);
       methodInfo.loaded = true;
 
-      return new RequestBuilder(GSON) //
-          .apiUrl(url)
-          .headers(headers)
-          .args(args.toArray(new Object[args.size()]))
-          .methodInfo(methodInfo)
-          .build();
+      RequestBuilder requestBuilder = new RequestBuilder(GSON, methodInfo);
+
+      for (Header header : headers) {
+        requestBuilder.addHeader(header.getName(), header.getValue());
+      }
+
+      requestBuilder.setApiUrl(url);
+      requestBuilder.setArguments(args.toArray(new Object[args.size()]));
+
+      return requestBuilder.build();
     }
 
     @SuppressWarnings("UnusedDeclaration") // Accessed via reflection.
diff --git a/retrofit/src/test/java/retrofit/RestAdapterTest.java b/retrofit/src/test/java/retrofit/RestAdapterTest.java
index e880cca809..bf6d603f0b 100644
--- a/retrofit/src/test/java/retrofit/RestAdapterTest.java
+++ b/retrofit/src/test/java/retrofit/RestAdapterTest.java
@@ -209,8 +209,7 @@ public void log(String message) {
 
   @Test public void asynchronousUsesExecutors() throws Exception {
     Response response = new Response(200, "OK", NO_HEADERS, new TypedString("{}"));
-    when(mockClient.execute(any(Request.class))) //
-        .thenReturn(response);
+    when(mockClient.execute(any(Request.class))).thenReturn(response);
     Callback<Object> callback = mock(Callback.class);
 
     example.something(callback);
diff --git a/retrofit/src/test/java/retrofit/RestMethodInfoTest.java b/retrofit/src/test/java/retrofit/RestMethodInfoTest.java
index 3659165b54..064c450d97 100644
--- a/retrofit/src/test/java/retrofit/RestMethodInfoTest.java
+++ b/retrofit/src/test/java/retrofit/RestMethodInfoTest.java
@@ -32,7 +32,10 @@
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 import static org.fest.assertions.api.Assertions.assertThat;
 import static org.junit.Assert.fail;
-import static retrofit.RestMethodInfo.NO_BODY;
+import static retrofit.RestMethodInfo.ParamUsage.BODY;
+import static retrofit.RestMethodInfo.ParamUsage.HEADER;
+import static retrofit.RestMethodInfo.ParamUsage.PATH;
+import static retrofit.RestMethodInfo.ParamUsage.QUERY;
 import static retrofit.RestMethodInfo.RequestType.MULTIPART;
 import static retrofit.RestMethodInfo.RequestType.SIMPLE;
 
@@ -356,7 +359,7 @@ class Example {
     assertThat(methodInfo.requestUrl).isEqualTo("/foo");
   }
 
-  @Test public void singleQueryParam() {
+  @Test public void singlePathQueryParam() {
     class Example {
       @GET("/foo?a=b")
       Response a() {
@@ -383,15 +386,12 @@ class Example {
     RestMethodInfo methodInfo = new RestMethodInfo(method);
     methodInfo.init();
 
-    assertThat(methodInfo.requestUrlParam).isEmpty();
-    assertThat(methodInfo.requestQueryName).isEmpty();
-    assertThat(methodInfo.requestFormFields).isEmpty();
-    assertThat(methodInfo.requestMultipartPart).isEmpty();
-    assertThat(methodInfo.bodyIndex).isEqualTo(NO_BODY);
+    assertThat(methodInfo.requestParamNames).isEmpty();
+    assertThat(methodInfo.requestParamUsage).isEmpty();
     assertThat(methodInfo.requestType).isEqualTo(SIMPLE);
   }
 
-  @Test public void singleParam() {
+  @Test public void singleQueryParam() {
     class Example {
       @GET("/") Response a(@Query("a") String a) {
         return null;
@@ -402,12 +402,12 @@ class Example {
     RestMethodInfo methodInfo = new RestMethodInfo(method);
     methodInfo.init();
 
-    assertThat(methodInfo.requestQueryName).hasSize(1).containsSequence("a");
-    assertThat(methodInfo.bodyIndex).isEqualTo(NO_BODY);
+    assertThat(methodInfo.requestParamNames).hasSize(1).containsExactly("a");
+    assertThat(methodInfo.requestParamUsage).hasSize(1).containsExactly(QUERY);
     assertThat(methodInfo.requestType).isEqualTo(SIMPLE);
   }
 
-  @Test public void multipleParams() {
+  @Test public void multipleQueryParams() {
     class Example {
       @GET("/") Response a(@Query("a") String a, @Query("b") String b, @Query("c") String c) {
         return null;
@@ -418,8 +418,8 @@ class Example {
     RestMethodInfo methodInfo = new RestMethodInfo(method);
     methodInfo.init();
 
-    assertThat(methodInfo.requestQueryName).hasSize(3).containsSequence("a", "b", "c");
-    assertThat(methodInfo.bodyIndex).isEqualTo(NO_BODY);
+    assertThat(methodInfo.requestParamNames).hasSize(3).containsExactly("a", "b", "c");
+    assertThat(methodInfo.requestParamUsage).hasSize(3).containsExactly(QUERY, QUERY, QUERY);
     assertThat(methodInfo.requestType).isEqualTo(SIMPLE);
   }
 
@@ -434,11 +434,8 @@ class Example {
     RestMethodInfo methodInfo = new RestMethodInfo(method);
     methodInfo.init();
 
-    assertThat(methodInfo.requestUrlParam).containsOnly(new String[] { null });
-    assertThat(methodInfo.requestQueryName).containsOnly(new String[] { null });
-    assertThat(methodInfo.requestFormFields).containsOnly(new String[] { null });
-    assertThat(methodInfo.requestMultipartPart).containsOnly(new String[] { null });
-    assertThat(methodInfo.bodyIndex).isEqualTo(0);
+    assertThat(methodInfo.requestParamNames).hasSize(1).containsExactly(new String[] { null });
+    assertThat(methodInfo.requestParamUsage).hasSize(1).containsExactly(BODY);
     assertThat(methodInfo.requestType).isEqualTo(SIMPLE);
   }
 
@@ -453,11 +450,8 @@ class Example {
     RestMethodInfo methodInfo = new RestMethodInfo(method);
     methodInfo.init();
 
-    assertThat(methodInfo.requestUrlParam).containsOnly(new String[] { null });
-    assertThat(methodInfo.requestQueryName).containsOnly(new String[] { null });
-    assertThat(methodInfo.requestFormFields).containsOnly(new String[] { null });
-    assertThat(methodInfo.requestMultipartPart).containsOnly(new String[] { null });
-    assertThat(methodInfo.bodyIndex).isEqualTo(0);
+    assertThat(methodInfo.requestParamNames).hasSize(1).containsExactly(new String[] { null });
+    assertThat(methodInfo.requestParamUsage).hasSize(1).containsExactly(BODY);
     assertThat(methodInfo.requestType).isEqualTo(SIMPLE);
   }
 
@@ -484,11 +478,8 @@ class Example {
     RestMethodInfo methodInfo = new RestMethodInfo(method);
     methodInfo.init();
 
-    assertThat(methodInfo.requestUrlParam).containsExactly("a", null, "c");
-    assertThat(methodInfo.requestQueryName).containsExactly(null, null, null);
-    assertThat(methodInfo.requestFormFields).containsExactly(null, null, null);
-    assertThat(methodInfo.requestMultipartPart).containsExactly(null, null, null);
-    assertThat(methodInfo.bodyIndex).isEqualTo(1);
+    assertThat(methodInfo.requestParamNames).containsExactly("a", null, "c");
+    assertThat(methodInfo.requestParamUsage).containsExactly(PATH, BODY, PATH);
     assertThat(methodInfo.requestType).isEqualTo(SIMPLE);
   }
 
@@ -712,8 +703,8 @@ Response a(@Header("a") String a, @Header("b") String b) {
     RestMethodInfo methodInfo = new RestMethodInfo(method);
     methodInfo.init();
 
-    assertThat(Arrays.asList(methodInfo.requestParamHeader))
-      .isEqualTo(Arrays.asList("a", "b"));
+    assertThat(methodInfo.requestParamNames).containsExactly("a", "b");
+    assertThat(methodInfo.requestParamUsage).containsExactly(HEADER, HEADER);
   }
 
   @Test(expected = IllegalStateException.class)