HTTP-RPC is an open-source framework for simplifying development of REST applications. It allows developers to create and access web services using a convenient, RPC-like metaphor while preserving fundamental REST principles such as statelessness and uniform resource access.
The project currently includes support for implementing REST services in Java and consuming services in Java, Objective-C/Swift, or JavaScript. The server component provides a lightweight alternative to larger Java-based REST frameworks such as JAX-RS, and the consistent cross-platform client API makes it easy to interact with services regardless of target device or operating system.
HTTP-RPC services are accessed by applying an HTTP verb such as GET or POST to a target resource. The target is specified by a path representing the name of the resource, and is generally expressed as a noun such as /calendar or /contacts.
Arguments are provided either via the query string or in the request body, like an HTML form. Although services may produce any type of content, results are generally returned as JSON. Operations that do not return a value are also supported.
The GET method is used to retrive information from the server. GET arguments are passed in the query string. For example, the following request might be used to obtain data about a calendar event:
GET /calendar?eventID=101
This request might retrieve the sum of two numbers, whose values are specified by the a and b query arguments:
GET /math/sum?a=2&b=4
Alternatively, the argument values could be specified as a list rather than as two fixed variables:
GET /math/sum?values=1&values=2&values=3
In either case, the service would return the value 6 in response.
The POST method is typically used to add new information to the server. For example, the following request might be used to create a new calendar event:
POST /calendar
As with HTML forms, POST arguments are passed in the request body. If the arguments contain only text values, they can be encoded using the "application/x-www-form-urlencoded" MIME type:
title=Planning+Meeting&start=2016-06-28T14:00&end=2016-06-28T15:00
If the arguments contain binary data such as a JPEG or PNG image, the "multipart/form-data" encoding can be used.
While it is not required, POST requests that create resources often return a value that can be used to identify the resource for later retrieval, update, or removal.
The PUT method updates existing information on the server. PUT arguments are passed in the query string. For example, the following request might be used to modify the end date of a calendar event:
PUT /calendar?eventID=102&end=2016-06-28T15:30
PUT requests generally do not return a value.
The DELETE method removes information from the server. DELETE arguments are passed in the query string. For example, this request might be used to delete a calendar event:
DELETE /calendar?eventID=102
DELETE requests generally do not return a value.
Although the HTTP specification defines a large number of possible response codes, only a few are applicable to HTTP-RPC services:
- 200 OK - The request succeeded, and the response contains a value representing the result
- 204 No Content - The request succeeded, but did not produce a result
- 404 Not Found - The requested resource does not exist
- 405 Method Not Allowed - The resource exists, but does not support the requested method
- 406 Not Acceptable - The requested representation is not available
- 500 Internal Server Error - An error occurred while executing the method
Support currently exists for implementing HTTP-RPC services in Java, and consuming services in Java, Objective-C/Swift, or JavaScript. Services can also be accessed using standard HTML forms and command-line utilities such as curl.
For additional information and examples, please see the wiki.
The Java server library allows developers to create and publish HTTP-RPC web services in Java. It is distributed as a single (approximately 50k) JAR file that contains the following core classes:
org.httprpcWebService- abstract base class for HTTP-RPC servicesRPC- annotation that specifies a "remote procedure call", or web service methodRequestDispatcherServlet- servlet that dispatches requests to service instancesEncoder- interface representing a content encoderJSONEncoder- class that encodes a JSON responseEncoding- annotation that specifies a custom encoding
org.httprpc.beansBeanAdapter- adapter class that presents the contents of a Java Bean instance as a map, suitable for serialization to JSON
org.httprpc.sqlResultSetAdapter- adapter class that presents the contents of a JDBC result set as an iterable list, suitable for streaming to JSONParameters- class for simplifying execution of prepared statements
org.httprpc.utilIteratorAdapter- adapter class that presents the contents of an iterator as an iterable list, suitable for streaming to JSON
Additionally, the server library provides the following classes for use with templates, which allow response data to be declaratively transformed into alternate representations:
org.httprpcTemplate- annotation that associates a template with a service method
org.httprpc.templateTemplateEncoder- class for processing template documentsModifier- interface representing a template modifier
Each of these classes is discussed in more detail below.
The JAR file for the Java server implementation of HTTP-RPC can be downloaded here. Java 8 and a servlet container supporting servlet specification 3.1 (e.g. Tomcat 8) or later are required.
WebService is an abstract base class for HTTP-RPC web services. All services must extend this class and must provide a public, zero-argument constructor.
Service operations are defined by adding public methods to a concrete service implementation. The RPC annotation is used to flag a method as remotely accessible. This annotation associates an HTTP verb and a resource path with the method. All public annotated methods automatically become available for remote execution when the service is published.
For example, the following class might be used to implement the simple addition operations discussed in the previous section:
public class MathService extends WebService {
@RPC(method="GET", path="sum")
public double getSum(double a, double b) {
return a + b;
}
@RPC(method="GET", path="sum")
public double getSum(List<Double> values) {
double total = 0;
for (double value : values) {
total += value;
}
return total;
}
}
Note that both methods are mapped to the path /math/sum. The RequestDispatcherServlet class discussed in the next section selects the best method to execute based on the names of the provided argument values. For example, the following request would cause the first method to be invoked:
GET /math/sum?a=2&b=4
This request would invoke the second method:
GET /math/sum?values=1&values=2&values=3
Method arguments may be any of the following types:
byte/Byteshort/Shortint/Integerlong/Longfloat/Floatdouble/Doubleboolean/BooleanStringjava.net.URLjava.time.LocalDatejava.time.LocalTimejava.time.LocalDateTimejava.util.Datejava.util.List
URL arguments represent binary content, and can only be used with POST requests submitted using the "multipart/form-data" encoding. List arguments represent multi-value parameters. They may be used with any request type, but elements must be a supported simple type; e.g. List<Double> or List<URL>.
Omitting the value of a primitive parameter results in an argument value of 0 for that parameter. Omitting the value of a simple reference type parameter produces a null argument value for that parameter. Omitting all values for a list type parameter produces an empty list argument for the parameter.
Methods may return any of the following types:
byte/Byteshort/Shortint/Integerlong/Longfloat/Floatdouble/Doubleboolean/BooleanCharSequencejava.time.LocalDatejava.time.LocalTimejava.time.LocalDateTimejava.util.Datejava.util.Listjava.util.Map
Methods may also return void or Void to indicate that they do not produce a value.
Map implementations must use String values for keys. Nested structures are supported, but reference cycles are not permitted.
List and Map types are not required to support random access; iterability is sufficient. Additionally, List and Map types that implement AutoCloseable will be automatically closed after their values have been written to the output stream. This allows service implementations to stream response data rather than buffering it in memory before it is written.
For example, the ResultSetAdapter class wraps an instance of java.sql.ResultSet and exposes its contents as a forward-scrolling, auto-closeable list of map values. Closing the list also closes the underlying result set, ensuring that database resources are not leaked. ResultSetAdapter is discussed in more detail later.
WebService provides the following methods that allow an extending class to obtain additional information about the current request:
getLocale()- returns the locale associated with the current requestgetUserName()- returns the user name associated with the current request, ornullif the request was not authenticatedgetUserRoles()- returns a set representing the roles the user belongs to, ornullif the request was not authenticated
The values returned by these methods are populated via protected setters, which are called once per request by RequestDispatcherServlet. These setters are not meant to be called by application code. However, they can be used to facilitate unit testing of service implementations by simulating a request from an actual client.
HTTP-RPC services are published via the RequestDispatcherServlet class. This class is resposible for translating HTTP request parameters to method arguments, invoking the specified method, and serializing the return value to JSON.
Each servlet instance hosts a single HTTP-RPC service. The name of the service type is passed to the servlet via the "serviceClassName" initialization parameter. For example:
<servlet>
<servlet-name>MathServlet</servlet-name>
<servlet-class>org.httprpc.RequestDispatcherServlet</servlet-class>
<init-param>
<param-name>serviceClassName</param-name>
<param-value>com.example.MathService</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>MathServlet</servlet-name>
<url-pattern>/math/*</url-pattern>
</servlet-mapping>
A new service instance is created and initialized for each request (unless the service method is static, in which case no instance is necessary). RequestDispatcherServlet converts the request parameters to the expected argument types, invokes the method, and writes the return value to the response stream. Note that service classes must be compiled with the -parameters flag so their method parameter names are available at runtime.
Values for numeric and boolean arguments are converted to the appropriate type using the parse method of the associated wrapper class (e.g. Integer#parseInt()). Other argument types are handled as described below:
String: no coercion necessaryjava.net.URL: URL of temporary file containing uploaded contentjava.time.LocalDate: result of callingLocalDate#parse()java.time.LocalTime: result of callingLocalTime#parse()java.time.LocalDateTime: result of callingLocalDateTime#parse()java.util.Date: result of callingLong#parseLong(), thenDate(long)java.util.List: array list containing argument values coerced toListelement type
By default, RequestDispatcherServlet uses the JSONEncoder class to transform method results to JSON. Return values are mapped to their JSON equivalents as follows:
Numberor numeric primitive: numberBooleanorboolean: true/falseCharSequence: stringjava.time.LocalDate: string formatted as an ISO datejava.time.LocalTime: string formatted as an ISO timejava.time.LocalDateTime: string formatted as an ISO date/timejava.util.Date: long value representing the date's absolute timejava.util.List: arrayjava.util.Map: object
JSONEncoder can also be used by application code to write JSON data to arbitrary output streams.
If the method completes successfully and returns a value, an HTTP 200 status code is returned. If the method returns void or Void, HTTP 204 is returned.
If the requested resource does not exist, the servlet returns an HTTP 404 status code. If the resource exists but does not support the requested method, HTTP 405 is returned.
If any exception is thrown while executing the method, HTTP 500 is returned. If an exception is thrown while serializing the response, the output is truncated. In either case, the exception is logged.
Servlet security is provided by the underlying servlet container. See the Java EE documentation for more information.
The Encoding annotation is used to associate a custom encoder with a service method. This allows an application to effectively extend the set of supported return types.
The annotation defines a single element representing the type of the encoder that will be used to serialize the return value. This type must implement the Encoder interface. For example:
@RPC(method="GET", path="/customValue")
@Encoding(CustomEncoder.class)
public CustomType getCustomType() { ... }
All requests for /customValue will return the representation of CustomType as defined by the CustomEncoder type.
While custom encodings offer a great deal of flexibility, many common use cases can be addressed using the various adapter types provided by the framework. These adapters are discussed in more detail below.
Templates are another means for customizing a resource's representation. They are discussed in a later section.
The BeanAdapter class allows the contents of a Java Bean object to be returned from a service method. This class implements the Map interface and exposes any properties defined by the Bean as entries in the map, allowing custom data types to be serialized to JSON.
For example, the following Bean class might be used to represent basic statistical data about a collection of values:
public class Statistics {
private int count = 0;
private double sum = 0;
private double average = 0;
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public double getSum() {
return sum;
}
public void setSum(double sum) {
this.sum = sum;
}
public double getAverage() {
return average;
}
public void setAverage(double average) {
this.average = average;
}
}
Using this class, an implementation of a getStatistics() method might look like this:
@RPC(method="GET", path="statistics")
public Map<String, ?> getStatistics(List<Double> values) {
Statistics statistics = new Statistics();
int n = values.size();
statistics.setCount(n);
for (int i = 0; i < n; i++) {
statistics.setSum(statistics.getSum() + values.get(i));
}
statistics.setAverage(statistics.getSum() / n);
return new BeanAdapter(statistics);
}
Although the values are actually stored in the strongly typed Statistics object, the adapter makes the data appear as a map, allowing it to be returned to the caller as a JSON object; for example:
{
"average": 3.0,
"count": 3,
"sum": 9.0
}
Note that, if a property returns a nested Bean type, the property's value will be automatically wrapped in a BeanAdapter instance. Additionally, if a property returns a List or Map type, the value will be wrapped in an adapter of the appropriate type that automatically adapts its sub-elements. This allows service methods to return recursive structures such as trees.
The ResultSetAdapter class allows the result of a SQL query to be efficiently returned from a service method. This class implements the List interface and makes each row in a JDBC result set appear as an instance of Map, rendering the data suitable for serialization to JSON. It also implements the AutoCloseable interface, to ensure that the underlying result set is closed once all of the response data has been written.
ResultSetAdapter is forward-scrolling only; its contents are not accessible via the get() and size() methods. This allows query results to be returned to the caller directly, without any intermediate buffering. The service can simply execute a query, pass the result set to the adapter's constructor, and return the adapter instance:
@RPC(method="GET", path="data")
public ResultSetAdapter getData() throws SQLException {
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select * from some_table");
return new ResultSetAdapter(resultSet);
}
If a column's label contains a period, the value will be returned as a nested structure. For example, the following query might be used to retrieve a list of employee records:
SELECT first_name AS 'name.first', last_name AS 'name.last', title FROM employees
Because the aliases for the first_name and last_name columns contain a period, each row will contain a nested "name" structure instead of a flat collection of key/value pairs; for example:
[
{
"name": {
"first": "John",
"last": "Smith"
},
"title": "Manager"
},
...
]
The Parameters class provides a means for executing prepared statements using named parameter values rather than indexed arguments. Parameter names are specified by a leading : character. For example:
SELECT * FROM some_table
WHERE column_a = :a OR column_b = :b OR column_c = COALESCE(:c, 4.0)
The parse() method is used to create a Parameters instance from a SQL statement. It takes a string or reader containing the SQL text as an argument; for example:
Parameters parameters = Parameters.parse(sql);
The getSQL() method returns the parsed SQL in standard JDBC syntax:
SELECT * FROM some_table
WHERE column_a = ? OR column_b = ? OR column_c = COALESCE(?, 4.0)
This value is used to create the actual prepared statement:
PreparedStatement statement = DriverManager.getConnection(url).prepareStatement(parameters.getSQL());
Parameter values are specified via a map passed to the apply() method:
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("a", "hello");
arguments.put("b", 3);
parameters.apply(statement, arguments);
Since explicit creation and population of the argument map can be cumbersome, the WebService class provides the following static convenience methods to help simplify map creation:
public static <K> Map<K, ?> mapOf(Map.Entry<K, ?>... entries) { ... }
public static <K> Map.Entry<K, ?> entry(K key, Object value) { ... }
Using the convenience methods, the code that applies the parameter values can be reduced to the following:
parameters.apply(statement, mapOf(entry("a", "hello"), entry("b", 3)));
Once applied, the statement can be executed:
return new ResultSetAdapter(statement.executeQuery());
The IteratorAdapter class allows the content of an arbitrary cursor to be efficiently returned from a service method. This class implements the List interface and makes each item produced by an iterator appear to be an element of the list, rendering the data suitable for serialization to JSON.
Like ResultSetAdapter, IteratorAdapter implements the AutoCloseable interface. If the underlying iterator type also implements AutoCloseable, IteratorAdapter will ensure that the underlying cursor is closed so that resources are not leaked.
As with ResultSetAdapter, IteratorAdapter is forward-scrolling only, so its contents are not accessible via the get() and size() methods. This allows the contents of a cursor to be returned directly to the caller without any intermediate buffering.
IteratorAdapter is typically used to serialize result data produced by NoSQL databases. It can also be used to serialize the result of stream operations on Java collection types. For example:
@RPC(method="GET", path="stream")
public IteratorAdapter getStream() {
return new IteratorAdapter(listOf("a", "b", "c").stream().iterator());
}
Although data produced by an HTTP-RPC web service is usually returned to the caller as JSON, it can also be transformed into other representations via "templates". Templates are documents that describe an output format, such as HTML, XML, or CSV. They are merged with result data at execution time to create the final response that is sent back to the caller.
HTTP-RPC templates are based on the CTemplate system, which defines a set of "markers" that are replaced with values supplied by a "data dictionary" when the template is processed. The following CTemplate marker types are supported by HTTP-RPC:
- {{variable}} - injects a variable from the data dictionary into the output
- {{#section}}...{{/section}} - defines a repeating section of content
- {{>include}} - imports content specified by another template
- {{!comment}} - provides informational text about a template's content
The value returned by a service method represents the data dictionary. Usually, this will be an instance of java.util.Map whose keys represent the values provided by the dictionary. For example, a simple template for transforming the output of the getStatistics() method discussed earlier into HTML is shown below:
<html>
<head>
<title>Statistics</title>
</head>
<body>
<p>Count: {{count}}</p>
<p>Sum: {{sum}}</p>
<p>Average: {{average}}</p>
</body>
</html>
This method returns a map containing the result of some simple statistical calculations:
{
"average": 3.0,
"count": 3,
"sum": 9.0
}
At execution time, the "count", "sum", and "average" variable markers will be replaced by their corresponding values from the data dictionary, producing the following markup:
<html>
<head>
<title>Statistics</title>
</head>
<body>
<p>Count: 3.0</p>
<p>Sum: 9.0</p>
<p>Average: 3.0</p>
</body>
</html>
Although maps are often used to provide a template's data dictionary, this is not strictly required. Non-map values are automatically wrapped in a map instance and assigned a default name of ".". This name can be used to refer to the value in a template.
For example, the following template could be used to transform output of a method that returns a double value:
The value is {{.}}.
If the value returned by the method is the number 8, the resulting output would look like this:
The value is 8.
The Template annotation is used to associate a template document with a method. The annotation's value represents the name and type of the template that will be applied to the results. For example:
@RPC(method="GET", path="statistics")
@Template(name="statistics.html", contentType="text/html")
public Map<String, ?> getStatistics(List<Double> values) { ... }
The name element refers to the file containing the template definition. It is specified as a resource path relative to the service type.
The contentType element indicates the type of the content produced by the named template. It is used by RequestDispatcherServlet to identify the requested template. A specific representation is requested by appending a file extension associated with the desired MIME type to the service name in the URL; for example, /math/statistics.html. If the requested representation does not exist, the servlet returns an HTTP 406 status code.
The optional userAgent element can be used to associate a template with a particular user agent string. This value is a regular expression that is matched against the User-Agent header provided by the caller. The default value matches all user agents.
Note that it is possible to associate multiple templates with a single service method. For example, the following code associates an additional XML template with the getStatistics() method:
@RPC(method="GET", path="statistics")
@Template(name="statistics.html", contentType="text/html")
@Template(name="statistics.xml", contentType="application/xml")
public Map<String, ?> getStatistics(List<Double> values) { ... }
The TemplateEncoder class is responsible for merging a template document with a data dictionary. Although it is used internally by HTTP-RPC to transform annotated method results, it can also be used by application code to perform arbitrary transformations. See the Javadoc for more information.
Variable markers inject a variable from the data dictionary into the output. They can be used to refer to any simple dictionary value (i.e. number, boolean, or character sequence). Nested values can be referred to using dot-separated path notation; e.g. "name.first". Missing (i.e. null) values are replaced with the empty string in the generated output.
Variable names beginning with the @ character represent "resource references". Resources allow static template content to be localized. At execution time, the template processor looks for a resource bundle with the same base name as the service type, using the locale specified by the current HTTP request. If the bundle exists, it is used to provide a localized string value for the variable.
For example, the descriptive text from statistics.html could be localized as follows:
title=Statistics
count=Count
sum=Sum
average=Average
The template could be updated to refer to these string resources as shown below:
<html>
<head>
<title>{{@title}}</title>
</head>
<body>
<p>{{@count}}: {{count}}</p>
<p>{{@sum}}: {{sum}}</p>
<p>{{@average}}: {{average}}</p>
</body>
</html>
When the template is processed, the resource references will be replaced with their corresponding values from the resource bundle.
Variable names beginning with the $ character represent "context references". Context properties provide information about the context in which the request is executing. HTTP-RPC provides the following context values:
scheme- the scheme used to make the request; e.g. "http" or "https"serverName- the host name of the server to which the request was sentserverPort- the port to which the request was sentcontextPath- the context path of the web application handling the request
For example, the following markup uses the contextPath value to embed a product image in an HTML template:
<img src="{{$contextPath}}/images/{{productID}}.jpg"/>
The CTemplate specification defines a syntax for applying an optional set of "modifiers" to a variable. Modifiers are used to transform a variable's representation before it is written to the output stream; for example, to apply an escape sequence.
Modifiers are specified as shown below. They are invoked in order from left to right. An optional argument value may be included to provide additional information to the modifier:
{{variable:modifier1:modifier2:modifier3=argument:...}}
HTTP-RPC provides the following set of standard modifiers:
format- applies a format string^html,^xml- applies markup encoding to a value^json- applies JSON encoding to a value^csv- applies CSV encoding to a value^url- applies URL encoding to a value
For example, the following marker applies a format string to a value and then URL-encodes the result:
{{value:format=0x%04x:^url}}
In addition to printf()-style formatting, the format modifier also supports the following arguments:
currency- applies a currency formatpercent- applies a percent formatshortDate- applies a short date formatmediumDate- applies a medium date formatlongDate- applies a long date formatfullDate- applies a full date formatshortTime- applies a short time formatmediumTime- applies a medium time formatlongTime- applies a long time formatfullTime- applies a full time formatshortDateTime- applies a short date/time formatmediumDateTime- applies a medium date/time formatlongDateTime- applies a long date/time formatfullDateTime- applies a full date/time format
For example, this marker applies a medium date format to a date value named "date":
{{date:format=mediumDate}}
Applications may also define their own custom modifiers. Modifiers are created by implementing the Modifier interface, which defines the following method:
public Object apply(Object value, String argument, Locale locale);
The first argument to this method represents the value to be modified, and the second is the optional argument value following the = character in the modifier string. If a modifier argument is not specified, the value of argument will be null. The third argument contains the caller's locale.
For example, the following class implements a modifier that converts values to uppercase:
public class UppercaseModifier implements Modifier {
@Override
public Object apply(Object value, String argument, Locale locale) {
return value.toString().toUpperCase(locale);
}
}
Custom modifiers are registered by adding them to the modifier map returned by TemplateEncoder#getModifiers(). The map key represents the name that is used to apply a modifier in a template document. For example:
TemplateEncoder.getModifiers().put("uppercase", new UppercaseModifier());
Note that modifiers must be thread-safe, since they are shared and may be invoked concurrently by multiple template engines.
Section markers define a repeating section of content. The marker name must refer to a list value in the data dictionary. Content between the markers is repeated once for each element in the list. The element provides the data dictionary for each successive iteration through the section. If the list is missing (i.e. null) or empty, the section's content is excluded from the output.
For example, a service that provides information about homes for sale might return a list of available properties as follows:
[
{
"streetAddress": "17 Cardinal St.",
"listPrice": 849000,
"numberOfBedrooms": 4,
"numberOfBathrooms": 3
},
{
"streetAddress": "72 Wedgemere Ave.",
"listPrice": 1650000,
"numberOfBedrooms": 5,
"numberOfBathrooms": 3
},
...
]
A template to present these results in an HTML table is shown below. Dot notation is used to refer to the list itself, and variable markers are used to refer to the properties of the list elements. The format modifier is used to present the list price as a localized currency value:
<html>
<head>
<title>Property Listings</title>
</head>
<body>
<table>
<tr>
<td>Street Address</td>
<td>List Price</td>
<td># Bedrooms</td>
<td># Bathrooms</em></td>
</tr>
{{#.}}
<tr>
<td>{{streetAddress}}</td>
<td>{{listPrice:format=currency}}</td>
<td>{{numberOfBedrooms}}</td>
<td>{{numberOfBathrooms}}</td>
</tr>
{{/.}}
</table>
</body>
</html>
Include markers import content defined by another template. They can be used to create reusable content modules; for example, document headers and footers.
For example, the following template, hello.txt, includes another document named world.txt:
Hello, {{>world.txt}}!
When hello.txt is processed, the include marker will be replaced with the contents of world.txt. For example, if world.txt contains the text "World", the result of processing hello.txt would be the following:
Hello, World!
Includes inherit their context from the parent document, so they can refer to elements in the parent's data dictionary. This allows includes to be parameterized.
Includes can also be used to facilitate recursion. For example, an include that includes itself could be used to transform the output of a method that returns a hierarchical data structure:
public class TreeNode {
public String getName() { ... }
public List<TreeNode> getChildren() { ... }
}
The result of processing the following template, treenode.html, would be a collection of nested unordered list elements representing each of the nodes in the tree:
<ul>
{{#children}}
<li>
<p>{{name}}</p>
{{>treenode.html}}
</li>
{{/children}}
</ul>
Comment markers provide informational text about a template's content. They are not included in the final output. For example, when the following template is processed, only the content between the <p> tags will be included:
{{! Some placeholder text }}
<p>Lorem ipsum dolor sit amet.</p>
The Java client library enables Java applications (including Android) to consume HTTP-RPC web services. It is distributed as a JAR file that contains the following classes, discussed in more detail below:
org.httprpcWebServiceProxy- invocation proxy for HTTP-RPC servicesResultHandler- callback interface for handling resultsResult- abstract base class for typed resultsAuthentication- interface representing an authentication providerBasicAuthentication- HTTP basic authentication providerDecoder- interface representing a content decoderJSONDecoder- class that decodes a JSON response
The JAR file for the Java client implementation of HTTP-RPC can be downloaded here. Java 7 or later is required.
The WebServiceProxy class acts as a client-side invocation proxy for HTTP-RPC web services. Internally, it uses an instance of HttpURLConnection to send and receive data. POST requests are encoded as "multipart/form-data".
The WebServiceProxy constructor accepts the following arguments:
serverURL- an instance ofjava.net.URLrepresenting the URL of the serverexecutorService- an instance ofjava.util.concurrent.ExecutorServicethat is used to dispatch service requests
Optional connection and read timeout values may also be provided.
Service operations are initiated by calling the invoke() method:
public <V> Future<V> invoke(String method, String path, Map<String, ?> arguments,
ResultHandler<V> resultHandler) { ... }
This method takes the following arguments:
method- the HTTP method to executepath- the resource patharguments- a map containing the request arguments as key/value pairsresultHandler- an instance ofResultHandlerthat will be invoked upon completion of the service operation
A convenience method is also provided for executing operations that don't take any arguments:
public <V> Future<V> invoke(String method, String path,
ResultHandler<V> resultHandler) { ... }
Both variants of the invoke() method return an instance of java.util.concurrent.Future representing the invocation request. This object allows a caller to cancel an outstanding request, obtain information about a request that has completed, or block the current thread while waiting for an operation to complete.
Arguments may be of any type, and are generally converted to parameter values via the toString() method. However, the following argument types are given special consideration:
- Instances of
java.net.URLrepresent binary content. They behave similarly to<input type="file">tags in HTML and can only be used withPOSTrequests. - Instances of
java.util.Listrepresent multi-value parameters. They may be used with any request type; however, lists containing URL values are handled similarly to<input type="file" multiple>tags in HTML and and can only be used withPOSTrequests.
The result handler is called upon completion of the operation. ResultHandler is a functional interface whose single method, execute(), is defined as follows:
public void execute(V result, Exception exception);
On successful completion, the first argument will contain the result of the operation. This will typically be an instance of one of the following types or null, depending on the response returned by the server:
- string:
String - number:
Number - true/false:
Boolean - array:
java.util.List - object:
java.util.Map
The second argument will be null in this case. If an error occurs, the first argument will be null and the second will contain an exception representing the error that occurred.
Internally, WebServiceProxy uses the JSONDecoder class to deserialize JSON response data returned by a service operation. This class can also be used by application code to read JSON data from arbitrary input streams.
Since explicit creation and population of the argument map can be cumbersome, WebServiceProxy provides the following static convenience methods to help simplify map creation:
public static <K> Map<K, ?> mapOf(Map.Entry<K, ?>... entries) { ... }
public static <K> Map.Entry<K, ?> entry(K key, Object value) { ... }
Using these methods, argument map creation can be reduced from this:
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("a", 2);
arguments.put("b", 4);
to this:
Map<String, Object> arguments = mapOf(entry("a", 2), entry("b", 4));
A complete example is provided later.
Subclasses of WebServiceProxy can override the decodeResponse() method to provide custom deserialization behavior. For example, an Android client could override this method to support Bitmap data:
@Override
protected Object decodeResponse(InputStream inputStream, String contentType) throws IOException {
Object value;
if (contentType != null && contentType.startsWith("image/")) {
value = BitmapFactory.decodeStream(inputStream);
} else {
value = super.decodeResponse(inputStream, contentType);
}
return value;
}
By default, a result handler is called on the thread that executed the remote request, which in most cases will be a background thread. However, user interface toolkits generally require updates to be performed on the main thread. As a result, handlers typically need to "post" a message back to the UI thread in order to update the application's state. For example, a Swing application might call SwingUtilities#invokeAndWait(), whereas an Android application might call Activity#runOnUiThread() or Handler#post().
While this can be done in the result handler itself, WebServiceProxy provides a more convenient alternative. The protected dispatchResult() method can be overridden to process all result handler notifications. For example, the following Android-specific code ensures that all result handlers will be executed on the main UI thread:
serviceProxy = new WebServiceProxy(serverURL, Executors.newSingleThreadExecutor()) {
private Handler handler = new Handler(Looper.getMainLooper());
@Override
protected void dispatchResult(Runnable command) {
handler.post(command);
}
};
Similar dispatchers can be configured for other Java UI toolkits such as Swing, JavaFX, and SWT. Command line applications can generally use the default dispatcher, which simply performs result handler notifications on the current thread.
Result is an abstract base class for typed results. Using this class, applications can easily map untyped object data returned by a service operation to typed values. It provides the following constructor that is used to populate Java Bean property values from map entries:
public Result(Map<String, Object> properties) { ... }
For example, the following Java class might be used to provide a typed version of the statistical data returned by the getStatistics() method discussed earlier:
public class Statistics extends Result {
private int count;
private double sum;
private double average;
public Statistics(Map<String, Object> properties) {
super(properties);
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public double getSum() {
return sum;
}
public void setSum(double sum) {
this.sum = sum;
}
public double getAverage() {
return average;
}
public void setAverage(double average) {
this.average = average;
}
}
The map data returned by getStatistics() can be converted to a Statistics instance as follows:
serviceProxy.invoke("getStatistics", (Map<String, Object> result, Exception exception) -> {
Statistics statistics = new Statistics(result);
System.out.println(statistics.getCount());
System.out.println(statistics.getSum());
System.out.println(statistics.getAverage());
});
Additionally, the Result class provides the following method for accessing nested map values by key path:
public static <V> V getValue(Map<String, ?> root, String path) { ... }
For example, given the following JSON response data, a call to getValue(result, "foo.bar") would return 123:
{
"foo": {
"bar": 123
}
}
See the Javadoc for more information.
Although it is possible to use the java.net.Authenticator class to authenticate service requests, this class can be difficult to work with, especially when dealing with multiple concurrent requests or authenticating to multiple services with different credentials. It also requires an unnecessary round trip to the server if a user's credentials are already known up front, as is often the case.
HTTP-RPC provides an additional authentication mechanism that can be specified on a per-proxy basis. The Authentication interface defines a single method that is used to authenticate each request submitted by a proxy instance:
public interface Authentication {
public void authenticateRequest(HttpURLConnection connection);
}
Authentication providers are associated with a proxy instance via the setAuthentication() method. For example, the following code associates an instance of BasicAuthentication with a service proxy:
serviceProxy.setAuthentication(new BasicAuthentication("username", "password"));
The BasicAuthentication class is provided by the HTTP-RPC Java client library. Applications may provide custom implementations of the Authentication interface to support other authentication schemes.
The following code snippet demonstrates how WebServiceProxy can be used to access the resources of the hypothetical math service discussed earlier. It first creates an instance of the WebServiceProxy class and configures it with a pool of ten threads for executing requests. It then invokes the getSum(double, double) method of the service, passing a value of 2 for "a" and 4 for "b". Finally, it executes the getSum(List<Double>) method, passing the values 1, 2, and 3 as arguments:
// Create service proxy
URL serverURL = new URL("https://localhost:8443");
ExecutorService executorService = Executors.newFixedThreadPool(10);
WebServiceProxy serviceProxy = new WebServiceProxy(serverURL, executorService);
// Get sum of "a" and "b"
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("a", 2), entry("b", 4)), new ResultHandler<Number>() {
@Override
public void execute(Number result, Exception exception) {
// result is 6
}
});
// Get sum of all values
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("values", listOf(1, 2, 3))), new ResultHandler<Number>() {
@Override
public void execute(Number result, Exception exception) {
// result is 6
}
});
Note that, in Java 8 or later, lambda expressions can be used instead of anonymous classes to implement result handlers, reducing the invocation code to the following:
// Get sum of "a" and "b"
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("a", 2), entry("b", 4)), (result, exception) -> {
// result is 6
});
// Get sum of all values
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("values", listOf(1, 2, 3))), (result, exception) -> {
// result is 6
});
The Objective-C/Swift client library enables iOS applications to consume HTTP-RPC services. It is delivered as a modular framework that includes the following types, discussed in more detail below:
WSWebServiceProxy- invocation proxy for HTTP-RPC servicesWSAuthentication- interface for authenticating requestsWSBasicAuthentication- authentication implementation supporting basic HTTP authentication
The framework for the Objective-C/Swift client can be downloaded here. It is also available via CocoaPods. iOS 8 or later is required.
The WSWebServiceProxy class serves as an invocation proxy for HTTP-RPC services. Internally, it uses an instance of NSURLSession to issue HTTP requests. POST requests are encoded as "multipart/form-data". NSJSONSerialization is used to decode JSON response data, and UIImage is used to decode image content. All other content is returned as NSData.
Service proxies are initialized via the initWithSession:serverURL: method, which takes an NSURLSession instance and the URL of the server as arguments. Service operations are executed by calling the invoke:path:arguments:resultHandler: method:
- (NSURLSessionDataTask *)invoke:(NSString *)method path:(NSString *)path
arguments:(NSDictionary<NSString *, id> *)arguments
resultHandler:(void (^)(id _Nullable, NSError * _Nullable))resultHandler;
This method takes the following arguments:
method- the HTTP method to executepath- the resource patharguments- a dictionary containing the request arguments as key/value pairsresultHandler- a callback that will be invoked upon completion of the method
A convenience method is also provided for executing operations that don't take any arguments:
- (NSURLSessionDataTask *)invoke:(NSString *)method path:(NSString *)path
resultHandler:(void (^)(id _Nullable, NSError * _Nullable))resultHandler;
Both variants return an instance of NSURLSessionDataTask representing the invocation request. This allows an application to cancel a task, if necessary.
Arguments may be of any type, and are generally converted to parameter values via the description method. However, the following argument types are given special consideration:
- Instances of
NSURLrepresent binary content. They behave similarly to<input type="file">tags in HTML and can only be used withPOSTrequests. - Instances of
NSArrayrepresent multi-value parameters. They may be used with any request type; however, arrays containing URL values are handled similarly to<input type="file" multiple>tags in HTML and and can only be used withPOSTrequests. - The
CFBooleanRefconstantskCFBooleanTrueandkCFBooleanFalseare converted to "true" and "false", respectively.
The result handler callback is called upon completion of the operation. The callback takes two arguments: a result object and an error object. If the operation completes successfully, the first argument will contain the result of the operation. If the operation fails, the second argument will be populated with an instance of NSError describing the error that occurred.
Note that, while requests are typically processed on a background thread, result handlers are called on the same operation queue that initially invoked the service method. This is typically the application's main queue, which allows result handlers to update the application's user interface directly, rather than posting a separate update operation to the main queue.
Although it is possible to use the URLSession:task:didReceiveChallenge:completionHandler: method of the NSURLSessionDataDelegate protocol to authenticate service requests, this method requires an unnecessary round trip to the server if a user's credentials are already known up front, as is often the case.
HTTP-RPC provides an additional authentication mechanism that can be specified on a per-proxy basis. The WSAuthentication protocol defines a single method that is used to authenticate each request submitted by a proxy instance:
- (void)authenticateRequest:(NSMutableURLRequest *)request;
Authentication providers are associated with a proxy instance via the authentication property of the WSWebServiceProxy class. For example, the following code associates an instance of WSBasicAuthentication with a service proxy:
serviceProxy.authentication = WSBasicAuthentication(username: "username", password: "password")
The WSBasicAuthentication class is provided by the HTTP-RPC framework. Applications may provide custom implementations of the WSAuthentication protocol to support other authentication schemes.
The following code snippet demonstrates how WSWebServiceProxy can be used to access the methods of the hypothetical math service. It first creates an instance of the WSWebServiceProxy class backed by a default URL session and a delegate queue supporting ten concurrent operations. It then invokes the getSum(double, double) method of the service, passing a value of 2 for "a" and 4 for "b". Finally, it executes the getSum(List<Double>) method, passing the values 1, 2, and 3 as arguments:
// Configure session
let configuration = URLSessionConfiguration.default
let delegateQueue = OperationQueue()
delegateQueue.maxConcurrentOperationCount = 10
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: delegateQueue)
// Initialize service proxy and invoke methods
let serviceProxy = WSWebServiceProxy(session: session, serverURL: URL(string: "https://localhost:8443")!)
// Get sum of "a" and "b"
serviceProxy.invoke("GET", path: "/math/sum", arguments: ["a": 2, "b": 4]) {(result, error) in
// result is 6
}
// Get sum of all values
serviceProxy.invoke("GET", path: "/math/sum", arguments: ["values": [1, 2, 3, 4]]) {(result, error) in
// result is 6
}
The JavaScript HTTP-RPC client enables browser-based applications to consume HTTP-RPC services. It is delivered as a JavaScript source file that contains a single WebServiceProxy class, discussed in more detail below.
The source code for the JavaScript client can be downloaded here.
The WebServiceProxy class serves as an invocation proxy for HTTP-RPC services. Internally, it uses an instance of XMLHttpRequest to communicate with the server, and uses JSON.parse() to convert the response to an object. POST requests are encoded using the "application/x-www-form-urlencoded" MIME type.
Service proxies are initialized via the WebServiceProxy constructor, which takes a single optional argument representing the request timeout. Service operations are executed by calling the invoke() method on the proxy instance:
WebServiceProxy.prototype.invoke = function(method, path, arguments, resultHandler) { ... }
This method takes the following arguments:
method- the HTTP method to executepath- the resource patharguments- an object containing the request arguments as key/value pairsresultHandler- a callback that will be invoked upon completion of the method
Arguments may be of any type, and are generally converted to parameter values via toString(). However, array arguments represent multi-value parameters, and behave similarly to <select multiple> tags in HTML forms.
The result handler is invoked upon completion of the operation. The callback takes two arguments: a result object and an error object. If the remote method completes successfully, the first argument contains the value returned by the method. If the method call fails, the second argument will contain the HTTP status code corresponding to the error that occurred.
The invoke() method returns the XMLHttpRequest instance used to execute the remote call. This allows an application to cancel a request, if necessary.
The following code snippet demonstrates how WebServiceProxy can be used to access the methods of the hypothetical math service. It first creates an instance of the WebServiceProxy class, and then invokes the getSum(double, double) method of the service, passing a value of 2 for "a" and 4 for "b". Finally, it executes the getSum(List<Double>) method, passing the values 1, 2, and 3 as arguments:
// Create service proxy
var serviceProxy = new WebServiceProxy();
// Get sum of "a" and "b"
serviceProxy.invoke("GET", "/math/sum", {a:4, b:2}, function(result, error) {
// result is 6
});
// Get sum of all values
serviceProxy.invoke("GET", "/math/sum", {values:[1, 2, 3, 4]}, function(result, error) {
// result is 6
});
For additional information and examples, see the the wiki.