Skip to content

Commit d70c341

Browse files
committed
Add support for request body content.
1 parent 690e831 commit d70c341

File tree

7 files changed

+283
-39
lines changed

7 files changed

+283
-39
lines changed

README.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,22 @@ protected String getKey(String name) { ... }
183183

184184
For example, given the preceding request, the key with name "contactID" would be "jsmith" and the key with name "addressType" would be "home".
185185

186+
### Custom Body Content
187+
The `Body` annotation can be used to associate custom body content with a service method. Annotated methods can access decoded content via the `getBody()` method. For example:
188+
189+
```java
190+
@RequestMethod("POST")
191+
@Body(Content.class)
192+
public uploadContent() {
193+
Content content = getBody();
194+
195+
...
196+
}
197+
```
198+
By default, body data is assumed to be JSON. However, subclasses can override the `decodeBody()` method to support other representations.
199+
200+
The decoded body is coerced to the declared type using the `BeanAdapter#adapt()` method, which is discussed in more detail later. If the decoded content cannot be converted to the specified type, an HTTP 415 response will be returned.
201+
186202
### Return Values
187203
Return values are converted to their JSON equivalents as follows:
188204

@@ -309,7 +325,7 @@ The `WebServiceProxy` class is used to issue API requests to a server. A Swift v
309325
* `method` - the HTTP method to execute
310326
* `url` - the URL of the requested resource
311327

312-
Request headers and arguments are specified via the `setHeaders()` and `setArguments()` methods, respectively. Like HTML forms, arguments are submitted either via the query string or in the request body. Arguments for `GET`, `PUT`, and `DELETE` requests are always sent in the query string. `POST` arguments are typically sent in the request body, and may be submitted as either "application/x-www-form-urlencoded" or "multipart/form-data" (specified via the proxy's `setEncoding()` method). However, if the request body is provided via a custom request handler (specified via the `setRequestHandler()` method), `POST` arguments will be sent in the query string.
328+
Request headers and arguments are specified via the `setHeaders()` and `setArguments()` methods, respectively. Like HTML forms, arguments are submitted either via the query string or in the request body. Arguments for `GET`, `PUT`, and `DELETE` requests are always sent in the query string. `POST` arguments are typically sent in the request body, and may be submitted as either "application/x-www-form-urlencoded" or "multipart/form-data" (specified via the proxy's `setEncoding()` method). However, if a custom body is provided via the `setBody()` method or the request body is generated by a custom request handler (specified via the `setRequestHandler()` method), `POST` arguments will be sent in the query string.
313329

314330
The `toString()` method is generally used to convert an argument to its string representation. However, `Date` instances are automatically converted to a long value representing epoch time. Additionally, `Iterable` instances represent multi-value parameters and behave similarly to `<select multiple>` tags in HTML. Further, when using the multi-part encoding, `URL` and `Iterable<URL>` values represent file uploads, and behave similarly to `<input type="file">` tags in HTML forms.
315331

@@ -356,15 +372,18 @@ The `adapt()` methods of the `WebServiceProxy` class can be used to facilitate t
356372

357373
```java
358374
public static <T> T adapt(URL baseURL, Class<T> type) { ... }
359-
public static <T> T adapt(URL baseURL, Class<T> type, Map<String, ?> headers) { ... }
360375
public static <T> T adapt(URL baseURL, Class<T> type, BiFunction<String, URL, WebServiceProxy> factory) { ... }
361376
```
362377

363-
All three versions take a base URL and an interface type as arguments and return an instance of the given type that can be used to invoke service operations. The second version accepts a map of HTTP header values that will be submitted with every service request. The third accepts a callback that is used to produce web service proxy instances. Interface types must be compiled with the `-parameters` flag so their method parameter names are available at runtime.
378+
Both versions take a base URL and an interface type as arguments and return an instance of the given type that can be used to invoke service operations. The second accepts a callback that is used to produce service proxy instances. Interface types must be compiled with the `-parameters` flag so their method parameter names are available at runtime.
379+
380+
The `RequestMethod` annotation is used to associate an HTTP verb with an interface method. The optional `ResourcePath` annotation can be used to associate the method with a specific path relative to the base URL. If unspecified, the method is associated with the base URL itself.
381+
382+
The `WebServiceProxy#setKeys()` method can be used to supply values for named path variables. Similarly, `WebServiceProxy#setHeaders()` and `WebServiceProxy#setBody()` can be used to provide header and body content, respectively, to an adapter instance. Values set via these methods will be submitted with each subsequent method invocation until they are cleared.
364383

365-
The `RequestMethod` annotation is used to associate an HTTP verb with an interface method. The optional `ResourcePath` annotation can be used to associate the method with a specific path relative to the base URL. If unspecified, the method is associated with the base URL itself. The `WebServiceProxy#setKeys()` method can be used to supply values for any named path variables.
384+
`POST` requests are generally submitted using the multi-part encoding or as JSON. However, this behavior can be overridden by a custom service proxy factory.
366385

367-
`POST` requests are always submitted using the multi-part encoding. Return values are handled as described for `WebServiceProxy`, and are automatically coerced to the correct type.
386+
Return values are handled as described for `WebServiceProxy`, and are automatically coerced to the correct type.
368387

369388
For example, the following interface might be used to model the operations of the math service:
370389

httprpc-client/src/main/java/org/httprpc/WebServiceProxy.java

Lines changed: 109 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import org.httprpc.beans.BeanAdapter;
1818
import org.httprpc.io.JSONDecoder;
19+
import org.httprpc.io.JSONEncoder;
1920

2021
import java.io.IOException;
2122
import java.io.InputStream;
@@ -34,6 +35,7 @@
3435
import java.util.Collections;
3536
import java.util.Date;
3637
import java.util.HashMap;
38+
import java.util.LinkedHashMap;
3739
import java.util.List;
3840
import java.util.Locale;
3941
import java.util.Map;
@@ -113,6 +115,9 @@ private static class TypedInvocationHandler implements InvocationHandler {
113115
BiFunction<String, URL, WebServiceProxy> factory;
114116

115117
Map<String, ?> keys = Collections.emptyMap();
118+
Map<String, ?> headers = Collections.emptyMap();
119+
120+
Object body = null;
116121

117122
TypedInvocationHandler(URL baseURL, Class<?> type, BiFunction<String, URL, WebServiceProxy> factory) {
118123
this.baseURL = baseURL;
@@ -170,18 +175,20 @@ public Object invoke(Object proxy, Method method, Object[] arguments) throws Thr
170175

171176
WebServiceProxy webServiceProxy = factory.apply(requestMethod.value(), url);
172177

178+
webServiceProxy.setHeaders(headers);
179+
173180
Parameter[] parameters = method.getParameters();
174181

175-
Map<String, Object> argumentMap = new HashMap<>();
182+
Map<String, Object> argumentMap = new LinkedHashMap<>();
176183

177184
for (int i = 0; i < parameters.length; i++) {
178-
Parameter parameter = parameters[i];
179-
180-
argumentMap.put(parameter.getName(), arguments[i]);
185+
argumentMap.put(parameters[i].getName(), arguments[i]);
181186
}
182187

183188
webServiceProxy.setArguments(argumentMap);
184189

190+
webServiceProxy.setBody(body);
191+
185192
return BeanAdapter.adapt(webServiceProxy.invoke(), method.getGenericReturnType());
186193
}
187194
}
@@ -218,6 +225,8 @@ public String toString() {
218225
private Map<String, ?> headers = emptyMap();
219226
private Map<String, ?> arguments = emptyMap();
220227

228+
private Object body;
229+
221230
private RequestHandler requestHandler = null;
222231

223232
private int connectTimeout = 0;
@@ -343,6 +352,26 @@ public void setArguments(Map<String, ?> arguments) {
343352
this.arguments = arguments;
344353
}
345354

355+
/**
356+
* Returns the request body.
357+
*
358+
* @return
359+
* A value representing the request body, or <code>null</code> for no body.
360+
*/
361+
public Object getBody() {
362+
return body;
363+
}
364+
365+
/**
366+
* Returns the request body.
367+
*
368+
* @param body
369+
* A value representing the request body, or <code>null</code> if no body has been set.
370+
*/
371+
public void setBody(Object body) {
372+
this.body = body;
373+
}
374+
346375
/**
347376
* Returns the request handler.
348377
*
@@ -441,7 +470,23 @@ public <T> T invoke() throws IOException {
441470
public <T> T invoke(ResponseHandler<T> responseHandler) throws IOException {
442471
URL url;
443472
RequestHandler requestHandler;
444-
if (method.equalsIgnoreCase("POST") && this.requestHandler == null) {
473+
if (body != null && this.requestHandler == null) {
474+
url = this.url;
475+
476+
requestHandler = new RequestHandler() {
477+
@Override
478+
public String getContentType() {
479+
return "application/json";
480+
}
481+
482+
@Override
483+
public void encodeRequest(OutputStream outputStream) throws IOException {
484+
JSONEncoder jsonEncoder = new JSONEncoder();
485+
486+
jsonEncoder.write(body, outputStream);
487+
}
488+
};
489+
} else if (method.equalsIgnoreCase("POST") && this.requestHandler == null) {
445490
url = this.url;
446491

447492
requestHandler = new RequestHandler() {
@@ -713,37 +758,13 @@ private static Object getParameterValue(Object argument) {
713758
* An instance of the given type that adapts the target service.
714759
*/
715760
public static <T> T adapt(URL baseURL, Class<T> type) {
716-
return adapt(baseURL, type, emptyMap());
717-
}
718-
719-
/**
720-
* Creates a type-safe web service proxy adapter.
721-
*
722-
* @param <T>
723-
* The target type.
724-
*
725-
* @param baseURL
726-
* The base URL of the web service.
727-
*
728-
* @param type
729-
* The type of the adapter.
730-
*
731-
* @param headers
732-
* A map of header values that will be submitted with service requests.
733-
*
734-
* @return
735-
* An instance of the given type that adapts the target service.
736-
*/
737-
public static <T> T adapt(URL baseURL, Class<T> type, Map<String, ?> headers) {
738761
return adapt(baseURL, type, (method, url) -> {
739762
WebServiceProxy webServiceProxy = new WebServiceProxy(method, url);
740763

741764
if (method.equalsIgnoreCase("POST")) {
742765
webServiceProxy.setEncoding(Encoding.MULTIPART_FORM_DATA);
743766
}
744767

745-
webServiceProxy.setHeaders(headers);
746-
747768
return webServiceProxy;
748769
});
749770
}
@@ -814,6 +835,65 @@ public static void setKeys(Object adapter, Map<String, ?> keys) {
814835
getTypedInvocationHandler(adapter).keys = keys;
815836
}
816837

838+
/**
839+
* Returns the keys associated with an adapter instance.
840+
*
841+
* @param adapter
842+
* The adapter instance.
843+
*
844+
* @return
845+
* The keys associated with the adapter instance.
846+
*/
847+
public static Map<String, ?> getHeaders(Object adapter) {
848+
return getTypedInvocationHandler(adapter).keys;
849+
}
850+
851+
/**
852+
* Sets the keys associated with an adapter instance.
853+
*
854+
* @param adapter
855+
* The adapter instance.
856+
*
857+
* @param headers
858+
* The keys to associate with the adapter instance.
859+
*/
860+
public static void setHeaders(Object adapter, Map<String, ?> headers) {
861+
if (headers == null) {
862+
throw new IllegalArgumentException();
863+
}
864+
865+
getTypedInvocationHandler(adapter).headers = headers;
866+
}
867+
868+
/**
869+
* Returns the request body associated with an adapter instance.
870+
*
871+
* @param adapter
872+
* The adapter instance.
873+
*
874+
* @return
875+
* The request body associated with the adapter instance, or <code>null</code>
876+
* if no request body has been set.
877+
*/
878+
@SuppressWarnings("unchecked")
879+
public static <T> T getBody(Object adapter) {
880+
return (T)getTypedInvocationHandler(adapter).body;
881+
}
882+
883+
/**
884+
* Sets the request body associated with an adapter instance.
885+
*
886+
* @param adapter
887+
* The adapter instance.
888+
*
889+
* @param body
890+
* The request body to associate with the adapter instance, or <code>null</code>
891+
* for no request body.
892+
*/
893+
public static void setBody(Object adapter, Object body) {
894+
getTypedInvocationHandler(adapter).body = body;
895+
}
896+
817897
private static TypedInvocationHandler getTypedInvocationHandler(Object adapter) {
818898
if (!(adapter instanceof Proxy)) {
819899
throw new IllegalArgumentException();
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.httprpc;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* Annotation that associates a body type with a service method.
10+
*/
11+
@Retention(RetentionPolicy.RUNTIME)
12+
@Target(ElementType.METHOD)
13+
public @interface Body {
14+
/**
15+
* @return
16+
* The body type.
17+
*/
18+
Class<?> value();
19+
}

0 commit comments

Comments
 (0)