HTTP-RPC 5.8 Released

HTTP-RPC is an open-source framework for implementing and interacting with RESTful and REST-like web 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 66KB in size, making it an ideal choice for applications where a minimal footprint is desired.

HTTP-RPC version 5.8 is now available for download and via Maven Central. This release adds support for automatically generated API documentation. For example, given the following service implementation:

@WebServlet(urlPatterns={"/math/*"})
public class MathService extends WebService {
    @RequestMethod("GET")
    @ResourcePath("sum")
    public double getSum(double a, double b) {
        return a + b;
    }
    
    @RequestMethod("GET")
    @ResourcePath("sum")
    public double getSum(List<Double> values) {
        double total = 0;
    
        for (double value : values) {
            total += value;
        }
    
        return total;
    }
}

a GET request for /math?api will now return a document that describes all of the service's endpoints and associated operations:

/math/sum

GET (a: double, b: double) -> double
GET (values: [double]) -> double

Services can additionally provide localized documentation for each method by including one or more resource bundles on the classpath. For example, the following MathService.properties file could be used to provide localized method descriptions for the MathService class:

MathService = Math example service.
getSum = Calculates the sum of two or more numbers.
getSum.a = The first number.
getSum.b = The second number.
getSum.values = The numbers to add.

The first line describes the service itself. The remaining lines describe the service methods and their parameters. A localized description of the math service might look like this:

Math example service.

/math/sum

GET (a: double, b: double) -> double

Calculates the sum of two or more numbers.

  • a The first number.
  • b The second number.
GET (values: [double]) -> double

Calculates the sum of two or more numbers.

  • values The numbers to add.

This allows consumers in any locale to easily discover and understand the operations supported by an endpoint.

For more information, see the project README.

Caching Web Service Response Data in iOS

NOTE 7/2/2018 The WSWebServiceProxy class mentioned in this article is now part of the Kilo project.

Many iOS applications obtain data via HTTP web services that return JSON documents. For example, the following code uses the HTTP-RPC WSWebServiceProxy class to invoke a simple web service that returns a simulated list of users as JSON. The results are stored as a dictionary and presented in a table view:

class UserViewController: UITableViewController {
    var users: [[String: Any]]! = nil

    ...

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        if (users == nil) {
            // Load the data from the server
            AppDelegate.serviceProxy.invoke("GET", path: "/users") { result, error in
                if (error == nil) {
                    self.users = result as? [[String: Any]]

                    self.tableView.reloadData()
                } 
            }
        }
    }

    ...
}

This works fine when both the device and the service are online, but it fails if either one is not. In some cases this may be acceptable, but other times it might be preferable to show the user the most recent response when more current data is not available.

To facilitate offline support, the response data must be cached. However, since writing to the file system is a potentially time-consuming operation, it should be done in the background to avoid blocking the main (UI) thread. Here, the data is written using an operation queue to ensure that access to it is serialized:

class UserViewController: UITableViewController {
    var users: [[String: Any]]! = nil

    var userCacheURL: URL?
    let userCacheQueue = OperationQueue()

    override func viewDidLoad() {
        super.viewDidLoad()

        ...

        if let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
            userCacheURL = cacheURL.appendingPathComponent("users.json")
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        if (users == nil) {
            // Load the data from the server
            AppDelegate.serviceProxy.invoke("GET", path: "/users") { result, error in
                if (error == nil) {
                    self.users = result as? [[String: Any]]

                    self.tableView.reloadData()

                    // Write the response to the cache
                    if (self.userCacheURL != nil) {
                        self.userCacheQueue.addOperation() {
                            if let stream = OutputStream(url: self.userCacheURL!, append: false) {
                                stream.open()

                                JSONSerialization.writeJSONObject(result!, to: stream, options: [.prettyPrinted], error: nil)

                                stream.close()
                            }
                        }
                    }
                } 
            }
        }
    }

    ...
}

Finally, the data can be retrieved from the cache if the web service call fails. The data is read from the cache in the background, and the UI is updated by reloading the table view on the main thread:

class UserViewController: UITableViewController {
    var users: [[String: Any]]! = nil

    var userCacheURL: URL?
    let userCacheQueue = OperationQueue()

    ...

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        if (users == nil) {
            // Load the data from the server
            AppDelegate.serviceProxy.invoke("GET", path: "/users") { result, error in
                if (error == nil) {
                    self.users = result as? [[String: Any]]

                    self.tableView.reloadData()

                    // Write the data to the cache
                    if (self.userCacheURL != nil) {
                        self.userCacheQueue.addOperation() {
                            if let stream = OutputStream(url: self.userCacheURL!, append: false) {
                                stream.open()

                                JSONSerialization.writeJSONObject(result!, to: stream, options: [.prettyPrinted], error: nil)

                                stream.close()
                            }
                        }
                    }
                } else if (self.userCacheURL != nil) {
                    // Read the data from the cache
                    self.userCacheQueue.addOperation() {
                        if let stream = InputStream(url: self.userCacheURL!) {
                            stream.open()

                            self.users = (try? JSONSerialization.jsonObject(with: stream, options: [])) as? [[String: Any]]

                            stream.close()
                        }

                        // Update the UI
                        OperationQueue.main.addOperation() {
                            self.tableView.reloadData()
                        }
                    }
                }
            }
        }
    }

    ...
}

Now, as long as the application has been able to connect to the server at least once, it can function either online or offline, using the cached response data.

For more ways to simplify iOS app development, please see my projects on GitHub:

  • MarkupKit – Declarative UI for iOS and tvOS
  • Kilo – Lightweight REST for iOS and tvOS