This is an assignment to the Software Architecture class at the Technische Hochschule Nürnberg.
In this assignment we will use Java annotations and reflection to interact with a remote REST (Representational State Transfer) API. As everyone (or maybe just me) loves Chuck Norris jokes we will implement a simple program to get random Chuck Norris jokes from the CNJDB (Chuck Norris Jokes Database).
- Create a fork of this repository (button in the right upper corner)
- Clone the project (get the link by clicking the green Clone or download button)
- Import the project to your IDE (remember the guide in assignment 1)
- Validate your environment by running the tests from your IntelliJ and by running
gradle test
on the command line.
When we started to use Gradle we already talked about dependency management. In this assignment we will use Gradle to manage the required libraries.
To complete this assignment you will need the following libraries:
With Gradle, project dependencies (both at compile and runtime) are specified in the build.gradle
file, in the dependencies
section.
Open the existing build.gradle file and inspect the dependencies
object (Gradle uses Groovy, a language similar to Java and Javascript).
Every dependency has a scope where it will be available.
To use a library across the whole project, declare it with the scope implementation
.
Gradle is designed to help you in all development phases and is extensible by plugins.
In the given build.gradle
are three plugins already applied:
java
: brings Java support to Gradle e.g. compilation)application
: enable you to run and package the application you will develop in this assignmentidea
: helps with IntelliJ import
To run the main
method in the App
class without IntelliJ you can now use the following Gradle command on the command line:
gradle run
The hard part of this assigment is you need to combine three parts to form the whole program:
- Gson for serialization
- Retrofit for HTTP requests
- A Gson type adapter to handle the status of the request response
It is strongly advised to read through the whole assignment and related documentations first; having the complete picture before starting with the parts helps a lot!
Google Gson is a library to serialize and deserialize JSON to or from Java objects.
The following code snippet shows the structure of a simple JSON object:
{
"id": "id-13434",
"value": "Ghosts are actually caused by Chuck Norris killing people faster than Death can process them.",
"categories": []
}
The most basic use case is to de/serialize objects; by defaut, Gson uses reflection to determine the properties.
class Joke {
String id;
String value;
String[] categories;
}
Gson gson = new Gson();
// JSON String --> Object
Joke j = gson.fromJson("{\"id\": 0, \"value\": \"Haha.\"}", Joke.class);
// categories remains `null`
// Objec --> JSON String
String json = gson.toJson(j);
Gson makes use of annotations to map JSON keys to fields of your class. Have a look at the docs and complete the model described in the following UML:
Hint: the given JSON object describes the exact structure of the JSON objects we want to deserialize. Use anntations to help gson map JSON fields to differently named Java field names.
- Import Gson to your project
- Familiarize yourself with Gson by trying a few examples
- Get familiar with the
@SerializedName
annotation
As you could see from the examples above (and in most cases), the response body of the CNJDB API looks like the following:
{
"categories": ["nerdy"],
"id": "irKXY3NtTXGe6W529sVlOg",
"value": "Chuck Norris can delete the Recycling Bin."
}
That is, if you implement your Joke
with matching fields (eg. using @SerializedName
), deserialization works out of the box.
Sometimes, however, the servers response isn't quite right, consider for example the output of
GET https://api.chucknorris.io/jokes/search?query=royalties`
{
"total": 1,
"result": [
{
"categories": [],
"created_at": "2020-01-05 13:42:19.104863",
"icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
"id": "o21pece2rn25d1cvm7eznq",
"updated_at": "2020-01-05 13:42:19.104863",
"url": "https://api.chucknorris.io/jokes/o21pece2rn25d1cvm7eznq",
"value": "Tom Clancy has to pay royalties to Chuck Norris because \"The Sum of All Fears\" is the name of Chuck Norris' autobiography."
}
]
}
That is, the actual data of interest (Joke[]
) is encapsulated in an object that has the fields total
and result
.
In this case, you have two options:
- Implement a wrapper data transfer object (DTO)
- Implement a custom type adapter, that converts the raw response into the desired return object.
For 1, this is straight forward
class SearchDTO {
int total;
Joke[] result;
}
For 2, let's look at how typeadapters work:
In a nutshell, a (Gson) type adapter is responsible to convert Java objects to JSON notation and vice versa. Key to this transformation is in the implementation of the following two methods:
public abstract class TypeAdapter<T> {
public abstract T read(final JsonReader reader);
public abstract void write(final JsonWriter writer, final T inst);
// ...
}
- Write a type adapter that accepts the response objects from CNJDB's search endpoint and outputs an instance of
Joke[]
. - Register the type adapter with your Retrofit instance
Note that you can use annotations on the
Joke
class, but you will have to write custom code to unwrap the joke from the response object. For this, you have two options:
- Implement a wrapper class, add appropriate fields, and return the
Joke[]
once unwrapped. - Unwrap the response object manually, by using the
reader
's.beginObject()
,.endObject()
and.next...()
methods to determine the number of jokes.
Note: There is no need to implement the
write
method, since we're only consuming the API, but not sending to it.
Check out this extensive tutorial on Gson type adapters.
Retrofit is a great library to implement HTTP clients.
To create an HTTP client, create an interface containing some methods you will call later to perform HTTP requests.
Retrofit also uses annotations to conveniently map these methods to API resource paths, e.g. getJokesBySearch("horse")
can be mapped to GET https://api.chucknorris.io/jokes/search?query=horse
.
Read through the Retrofit documentation and implement the CNJDBApi
interface as shown in the following UML:
- Start by implementing the method
getRandomJoke()
; use the appropriate annotations to decodate the interface method. - Modify the
main
method in theApp
class to create an instance of theCNJDBApi
usingRetrofit.Builder
. You need to add a converter factory that helps converting the JSON response to an object; you can set Gson usingGsonConverterFactory.create()
. - Print a random joke to
System.out
, and complete the test methodtestCollision
. Recall that you work withCall
objects that need to be executed before you can retrieve the response body. - After completing the
getRandomJoke()
method try to add the other methods. - If you are not sure if your query strings are correct you can test them within the command line using
curl
or in a browser extension such as Postman.
Most unix systems will provide the cURL program:
curl -X GET "https://api.chucknorris.io/jokes/random" -H "accept: application/json"
On Windows, you can use the PowerShell to accomplish the same like so:
(Invoke-WebRequest
-Uri https://api.chucknorris.io/jokes/random
-Headers @{"accept"="application/json"}
).Content | ConvertFrom-Json | ConvertTo-Json
(The part | ConvertFrom-Json | ConvertTo-Json
is only necessary for formatting.)
Remark: to execute this command you have to remove the newlines!