Short tutorial
This tutorial will go through the following steps:
- Setup the wrapper
- Get a token from an authorize code
- Get the current user
- Get the current users readings
- Update the latest of the current user's readings
Environment env = Environment.Live;
Selects which server to send requests. In 99% of the cases you'll want to use
Environment.Live
which is the standard Readmill API server.
ReadmillWrapper wrapper = new ReadmillWrapper("my-client-id", "my-client-secret", env);
Creates a wrapper with your client credentials that you got when you registered your app. You can register a new app or manage your current ones here: https://readmill.com/you/apps
If you're accessing authenticated endpoints, then you'll need to obtain an access token. This requires a browser for the user to log in to Readmill and authorize your application.
The wrapper can generate a url where you can send the user, but you need to have a way to caputer the authorization code returned from Readmill once the user allows your app. Once you have the code, then the wrapper can exchange that code for a token.
The full Readmill authorization process is described here
A token is associated with a callback uri and the authentication code received is valid only for that callback. So the first step is the inform the wrapper for which callback we want to retrieve a token.
URI myCallback = URI.create("https://my-readmill-hack.com/callback");
wrapper.setRedirectURI(myCallback);
The next step is to get a url of where to send users so that they can authorize our application.
URL authorizationUrl = wrapper.getAuthorizationUrl();
Once we have sent the user to that url, and gotten the response code from the callback url, then we can get the actual token.
String code = codeRetrievedFromTheCallbackAbove; // step is not included in this tutorial
wrapper.obtainToken(code);
Readmill also provides a way to obtain a token that does not expire. This is easier to implement since the refresh token flow is no longer needed – but it is also less secure so use that feature responsibly.
If you want to request a non expiring token you have to set the scope in the same way as the callback uri.
wrapper.setScope("non-expiring");
URL authorizationUrl = wrapper.getAuthorizationUrl();
// ... get the code
wrapper.obtainToken(code);
To get the user for the token, we need to look into constructing requests. The wrapper allows you to
construct complex requests using a RequestBuilder
interface that constructs a request by chaining multiple calls together.
You could also construct the requests yourself and pass then directly into the wrapper. In this tutorial we will only look at using the RequestBuilder
.
The final call for getting the current user is:
JSONObject user = wrapper.get("/me").fetch("user");
This call has two parts. First we start building a request by telling the wrapper which HTTP method we want, and the endpoint given as a string. In this example we want a GET
request to /me
.
Then we finish off by calling fetch("user")
. This does a few things. It ends the request building and sends the request to the server, parses the response as JSON and unwraps the top level object user
.
That last bit probably needs a little elaboration. All responses from Readmill are wrapped in a key that determines the type of the object. For example, the /me
endpoint above returns a user, and the json
response from Readmill then loooks like this:
{ "user": { id: 1, "username": "christoffer", ... } }
The Readmill API is consistent in formatting their responses like this, so to
you might find yourself writing a lot of code that looks like this:
.fetch().getOptJSONObject("user")
. This pattern in fact so common that the wrapper
provides a shortcut to unwrap a top level object directly: fetch("user")
.
In order to get the 10 latest readings of the current user, we use the id
from the
fetched user, and construct a little longer request by chaining a couple of parameters together.
JSONArray latestReadings;
String myReadingsUrl = String.format("/users/%d/readings", me.getInt("id");
latestReadings = wrapper.get(myReadingsUrl).
order("created_at").count(10).
fetchItems("reading");
As with single objects, the Readmill API wraps collections under a top level key. As this key is always items
the wrapper provides a shortcut for unwrapping collections and returning them as a JSONArray: fetchItems()
. Calling fetchItems()
is equivalent to calling fetch().optJSONArray("items")
.
However, objects in collections are also wrapped under their type as the top level key. For example, a collection of readings could look something like:
{
"items": [
{ "reading": { "id": 123, "state": "reading" } },
{ "reading": { "id": 456, "state": "finished" } },
]
}
If all the objects are of the same type, we can take yet another shortcut by passing the objects top level key to fetchItems()
, like this: fetchItems("reading")
. This unwraps the items
and each contained reading
all in one swoop.
The example above would thus be transformed by calling fetchItems("reading")
into:
[
{ "id": 123, "state": "reading" },
{ "id": 456, "state": "finished" },
]
Now we know enough to do the final step of the tutorial: get the most recent reading from the array and finish it with a closing remark.
JSONObject mostRecentReading = latestReadings.optJSONObject(0);
String mostRecentReadingUrl = String.format("/readings/%d", mostRecentReading.getInt("id"));
latestReading = wrapper.put(mostRecentReadingUrl). // send a PUT request to the url
readingState("finished"). // set the reading state to "finished"
readingClosingRemark("Amazing book, front to cover"). // set the closing remark
fetchJSON("reading"); // send the request and get the response
Left out from this tutorial, to promote simplicity, is any kind of error handling. The return value of the
methods used above (fetch()
, fetchItems()
etc.) all return null
if there is an error either with sending
the request or parsing the response as JSON. For simple cases this is usually enough, but for more robust implementations
there are alternatives that throw errors fetchAndThrow()
and fetchItemsAndThrow()
respectively.
If you want even more control you can construct requests using the Request
object and pass then to one of
get()
, post()
, etc. The return of those methods will be the bare HTTPResponse
object, and they all throw
IOError
when the requests could not be sent.
As an example, here is the final call of the tutorial with a more manual approach:
try {
JSONObject mostRecentReading = latestReadings.getJSONObject(0);
} catch(JSONException e) {
throw new RuntimeException("Invalid JSON in response from Readmill");
}
String mostRecentReadingUrl = String.format("/readings/%d", mostRecentReading.getInt("id"));
Request closeReadingRequest = Request.
to(mostRecentReadingUrl.toString()).
withParams(
"reading[state]", "finished",
"reading[closing_remark]", "Amazing book, front to cover");
try {
HTTPReponse response = wrapper.put(closeReadingRequest);
if(response.getStatusLine().getStatusCode() == 200) {
JSONObject updatedReading = HTTPUtils.getJSON(response);
} else {
throw new RuntimeException("Change was not accepted by the server");
}
} catch(IOException e) {
throw new RuntimeException("A network error occurred while sending the request");
} catch(JSONException e) {
throw new RuntimeException("The server gave an unexpected response");
}