HTTP-RPC 5.5 Released

HTTP-RPC is an open-source framework for implementing REST services in Java. It is extremely lightweight and requires only a Java runtime environment and a servlet container. The entire framework is distributed as a single JAR file that is about 52KB in size, making it an ideal choice for applications such as microservices where a minimal footprint is desired.

HTTP-RPC version 5.5 is now available for download and via Maven Central. The primary enhancement in this release is the new WebServiceProxy class, which allows an HTTP-RPC web service to act as a consumer of other REST services.

Service proxies are initialized via a constructor that takes the following arguments:

  • method – the HTTP method to execute
  • url – an instance of java.net.URL representing the target of the operation

Request headers and arguments are specified via the proxy's getHeaders() and getArguments() methods, respectively. Like HTML forms, arguments are submitted either via the query string or in the request body. As with HTML, POST requests may be submitted as either "application/x-www-form-urlencoded" or "multipart/form-data" (specified via the proxy's setEncoding() method). Custom request encodings are also supported.

Service operations are invoked via one of the following methods:

public <T> T invoke() throws IOException { ... }
public <T> T invoke(ResponseHandler<T> responseHandler) throws IOException { ... }

The first version automatically deserializes a successful response using HTTP-RPC's JSONDecoder class. The second version allows a caller to perform custom deserialization of the server response.

For example, the following code snippets demonstrate how WebServiceProxy might be used to access the operations of a simple math service:

// GET /math/sum?a=2&b=4
WebServiceProxy webServiceProxy = new WebServiceProxy("GET", new URL("http://localhost:8080/httprpc-test/math/sum"));

webServiceProxy.getArguments().put("a", 4);
webServiceProxy.getArguments().put("b", 2);

Number result = webServiceProxy.invoke();

System.out.println(result); // 6.0
// GET /math/sum?values=1&values=2&values=3
WebServiceProxy webServiceProxy = new WebServiceProxy("GET", new URL("http://localhost:8080/httprpc-test/math/sum"));

webServiceProxy.getArguments().put("values", Arrays.asList(1, 2, 3));

Number result = webServiceProxy.invoke();

System.out.println(result); // 6.0

Typed Web Service Access

Additionally, the adapt() methods of the WebServiceProxy class can be used to facilitate type-safe access to web services:

public static <T> T adapt(URL baseURL, Class<T> type) { ... }
public static <T> T adapt(URL baseURL, Class<T> type, Map<String, ?> headers) { ... }

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 version also accepts a map of header values that will be submitted with every service request.

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.

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

public interface MathService {
    @RequestMethod("GET")
    @ResourcePath("sum")
    public Number getSum(double a, double b) throws IOException;

    @RequestMethod("GET")
    @ResourcePath("sum")
    public Number getSum(List<Double> values) throws IOException;
}

This code snippet uses the adapt() method to create an instance of MathService, then invokes the getSum() methods on the returned instance. The results are identical to the previous example:

MathService mathService = WebServiceProxy.adapt(new URL("http://localhost:8080/httprpc-test/math/"), MathService.class);

// GET /math/sum?a=2&b=4
mathService.getSum(4, 2); // 6.0

// GET /math/sum?values=1&values=2&values=3
mathService.getSum(Arrays.asList(1.0, 2.0, 3.0)); // 6.0

Summary

This article introduced HTTP-RPC's new WebServiceProxy class, which allows an HTTP-RPC service to act as a consumer of other REST services. For more information, see the project README.

HTTP-RPC 5.3 Released

HTTP-RPC 5.3 is now available for download. This release adds support for typed result set iteration.

As previously discussed, HTTP-RPC's ResultSetAdapter class can be used to optimize transformation of JDBC result set data to JSON. The new adapt() method the 5.3 release adds to ResultSetAdapter can be used to facilitate type-safe iteration of query results. This method takes a ResultSet and an interface type as arguments, and returns an Iterable of the given type representing the rows in the result set.

For example, this interface might be used to model the results of a query on the "pet" table from the MySQL sample database:

public interface Pet {
    public String getName();
    public String getOwner();
    public String getSpecies();
    public String getSex();
    public Date getBirth();
}

The following service method uses adapt() to create an iterable sequence of Pet values. It wraps the adapter's iterator in a stream, and then uses the stream to calculate the average age of all pets in the database. The getBirth() method declared by the Pet interface is used to retrieve each pet's age in epoch time. The average value is converted to years at the end of the method:

@RequestMethod("GET")
@ResourcePath("/average-age")
public double getAverageAge() throws SQLException {
    Date now = new Date();

    double averageAge;
    try (Connection connection = DriverManager.getConnection(DB_URL);
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery("SELECT birth FROM pet")) {
        Iterable<Pet> pets = ResultSetAdapter.adapt(resultSet, Pet.class);

        Stream<Pet> stream = StreamSupport.stream(pets.spliterator(), false);

        averageAge = stream.mapToLong(pet -> now.getTime() - pet.getBirth().getTime()).average().getAsDouble();
    }

    return averageAge / (365.0 * 24.0 * 60.0 * 60.0 * 1000.0);
}

For more information, see the project README.

Introducing Kilo

Kilo is an open-source framework for consuming REST services in iOS or tvOS. It is extremely lightweight and provides a convenient, callback-based interface that makes it easy to interact with remote APIs.

For example, the following code snippet shows how a client application might access a simple service that returns a friendly greeting. The request is executed asynchronously, and the result is printed when the call returns:

webServiceProxy.invoke(.get, path: "/hello") { (result: String?, error: Error?) in
    if let greeting = result {
        print(greeting) // "Hello, World!"
    }
}

The project’s name comes from the nautical K or Kilo flag, which means “I wish to communicate with you”:

This article introduces the Kilo framework and provides an overview of its key features.

WebServiceProxy Class

Kilo is distributed as a universal binary that will run in the iOS simulator as well as on an actual device. The framework contains a single class named WebServiceProxy that is used to issue API requests to the server.

Service proxies are initialized via init(session:serverURL:), which takes the following arguments:

  • session – a URLSession instance that is used to create service requests
  • serverURL – the base URL of the service

A service operation is initiated via one of the following methods:

public func invoke(_ method: Method, path: String,
    arguments: [String: Any] = [:], content: Data? = nil, contentType: String? = nil,
    resultHandler: @escaping (_ result: T?, _ error: Error?) -> Void) -> URLSessionTask? { ... }

public func invoke(_ method: Method, path: String,
    arguments: [String: Any] = [:], content: Data? = nil, contentType: String? = nil,
    resultHandler: @escaping (_ result: T?, _ error: Error?) -> Void) -> URLSessionTask? { ... }

public func invoke(_ method: Method, path: String,
    arguments: [String: Any] = [:], content: Data? = nil, contentType: String? = nil,
    responseHandler: @escaping (_ content: Data, _ contentType: String?) throws -> T?,
    resultHandler: @escaping (_ result: T?, _ error: Error?) -> Void) -> URLSessionTask? { ... }

All three methods accept the following arguments:

  • method – the HTTP method to execute
  • path – the path to the requested resource
  • arguments – a dictionary containing the method arguments as key/value pairs
  • content – an optional Data instance representing the body of the request
  • contentType – an optional string value containing the MIME type of the content
  • resultHandler – a callback that will be invoked upon completion of the method

The first version of the method uses JSONSerialization to decode response data. The second uses JSONDecoder to return a decodable value. The third version accepts an additional responseHandler argument to facilitate decoding of custom response content (for example, a UIImage).

All three methods return an instance of URLSessionTask representing the invocation request. This allows an application to cancel a task, if necessary.

Arguments

Like HTML forms, arguments are submitted either via the query string or in the request body. Arguments for GET, PUT, PATCH, 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” (determined via the service proxy’s encoding property). However, if a custom body is specified via the content parameter, POST arguments will be sent in the query string.

Any value that provides a description property may be used as an argument. This property is generally used to convert the argument to its string representation. However, Date instances are automatically converted to a 64-bit integer value representing epoch time (the number of milliseconds that have elapsed since midnight on January 1, 1970).

Additionally, array instances represent multi-value parameters and behave similarly to tags in HTML. Further, when using the multi-part form data encoding, instances of URL represent file uploads and behave similarly to tags in HTML forms. Arrays of URL values operate similarly to tags.

Return Values

The result handler is called upon completion of the operation. If successful, the first argument will contain a deserialized representation of the content returned by the server, and the second argument will be nil. Otherwise, the first argument will be nil, and the second will be populated with an Error instance describing the problem that occurred.

Note that, while service requests are typically processed on a background thread, result handlers are always executed on the application’s main thread. This allows result handlers to update the user interface directly, rather than posting a separate update operation to the main queue.

If the server returns an error response, a localized description of the error will be provided in the localized description of the error parameter. Further, if the error is returned with a content type of “text/plain”, the response body will be returned in the error’s debug description.

Example

The following code snippet demonstrates how the WebServiceProxy class might be used to access the operations of a simple math service:

// Create service proxy
let webServiceProxy = WebServiceProxy(session: URLSession.shared, serverURL: URL(string: "http://localhost:8080")!)

// Get sum of "a" and "b"
webServiceProxy.invoke(.get, path: "/math/sum", arguments: [
    "a": 2,
    "b": 4
]) { (result: Int?, error: Error?) in
    // result is 6
}

// Get sum of all values
webServiceProxy.invoke(.get, path: "/math/sum", arguments: [
    "values": [1, 2, 3, 4]
]) { (result: Int?, error: Error?) in
    // result is 10
}

Additional Information

This article introduced the Kilo framework and provided an overview of its key features. For additional information, see the the project README.