I just announced the Master Class of my "REST With Spring" Course:
1. Overview
The purpose of this tutorial is to explore the Play Framework and learn how to build REST services with it using Java.
We will put together a REST API to create, retrieve, update and delete student records.
In such applications, we would normally have a database to store student records. Play has an inbuilt H2 database along with support for JPA with Hibernate and other persistence frameworks.
However, to keep things simple and focus on the most important stuff, we will use a simple map to store student objects with unique Ids.
2. Play Framework Setup
Let’s head over to Play framework’s official page and download the latest version of its distribution. At the time of this tutorial, the latest is version 2.5.
This will download a small starter package with a script called activator inside the bin folder. This script will setup the rest of the framework for us incrementally as and when we need the different components.
To make things simple, we will need to add the activator to PATH variable so as to run it from the console.
To get a more gentle introductory article for Play, you can follow this link.
3. Create New Application
Let’s create a new application called student-api based on play-Java template:
activator new student-api play-Java
We will only be discussing REST specific concepts. The rest of the code should be self-explanatory.
4. Project Dependencies
Play allows us to add external JARs as project dependencies in a number of ways. In this case, we will use the simplest which involves adding a JAR in the lib folder in the project’s root.
The only external dependency we will use, is the JSON library whose latest version we can download from Maven Central by following this link.
If student-api/lib folder does not already exist, let’s create it and drop the jar in there. However, this will only be used when we run our tests from within Play.
As it is with REST applications, we can as well start the server and run tests from any other environment by sending requests and making assertions on the results.
To make things simple, we will be using any IDE to run these tests. You can as well test the GET requests from the browser if you prefer.
5. Models
Let’s navigate to student-api/app/models or create one if it does not already exist. Let’s then create a Java bean for moving student information, here is Student.java:
public class Student { private String firstName; private String lastName; private int age; private int id; // standard constructors, getters and setters }
We will now create a simple data store for student data with helper methods to perform CRUD operations on it, here is StudentStore.java:
public class StudentStore { private Map<Integer, Student> students = new HashMap<>(); public Student addStudent(Student student) { int id = students.size(); student.setId(id); students.put(id, student); return student; } public Student getStudent(int id) { return students.get(id); } public Set<Student> getAllStudents() { return new HashSet<>(students.values()); } public Student updateStudent(Student student) { int id = student.getId(); if (students.containsKey(id)) { students.put(id, student); return student; } return null; } public boolean deleteStudent(int id) { return students.remove(id) != null; } }
We are storing students data in a hashmap. We create an integer id for each new student record and use it as a key.
6. Controllers
Let’s head over to student-api/app/controllers and create a new controller called StudentController.java. Let’s step through the code incrementally.
Note that Play ships with Jackson to allow for data processing – so we can import any Jackson classes we need without external dependencies.
Let’s define an utility class to perform repetitive operations. In this case, building HTTP responses. So let’s create student-api/app/util package and add Util.java in it:
public class Util { public static ObjectNode createResponse( Object response, boolean ok) { ObjectNode result = Json.newObject(); result.put("isSuccessfull", ok); if (response instanceof String) { result.put("body", (String) response); } else { result.put("body", (JsonNode) response); } return result; } }
With this method, we will be creating standard JSON responses with a boolean isSuccessfull key and the response body.
We can now step through the actions of the controller class.
The create Action:
public Result create() { JsonNode json = request().body().asJson(); if (json == null){ return badRequest(Util.createResponse( "Expecting Json data", false)); } Student student = StudentStore.getInstance().addStudent( (Student) Json.fromJson(json, Student.class)); JsonNode jsonObject = Json.toJson(student); return created(Util.createResponse(jsonObject, true)); }
We use a call from the super controller class to retrieve the request body into Jackson’s JsonNode class. Notice how we use the utility method to create a response if the body is null.
We can pass to it any String or a JsonNode and a boolean flag to indicate status.
Notice also how we use Json.fromJson() to convert the incoming JSON object into a Student object and back to JSON for the response.
Finally, instead of ok() which we are used to, we are using created helper method from the play.mvc.results package. The concepts will come up throughout the rest of actions.
The update Action:
public Result update() { JsonNode json = request().body().asJson(); if (json == null){ return badRequest(Util.createResponse( "Expecting Json data", false)); } Student student = StudentStore.getInstance().updateStudent( (Student) Json.fromJson(json, Student.class)); if (student == null) { return notFound(Util.createResponse( "Student not found", false)); } JsonNode jsonObject = Json.toJson(student); return ok(Util.createResponse(jsonObject, true)); }
The retrieve Action:
public Result retrieve(int id) { Student student = StudentStore.getInstance().getStudent(id); if (student == null) { return notFound(Util.createResponse( "Student with id:" + id + " not found", false)); } JsonNode jsonObjects = Json.toJson(student); return ok(Util.createResponse(jsonObjects, true)); }
The delete Action:
public Result delete(int id) { boolean status = StudentStore.getInstance().deleteStudent(id); if (!status) { return notFound(Util.createResponse( "Student with id:" + id + " not found", false)); } return ok(Util.createResponse( "Student with id:" + id + " deleted", true)); }
The listStudents Action:
public Result listStudents() { Set<Student> result = StudentStore.getInstance().getAllStudents(); ObjectMapper mapper = new ObjectMapper(); JsonNode jsonData = mapper.convertValue(result, JsonNode.class); return ok(Util.createResponse(jsonData, true)); }
Notice a difference in how we convert a list of Student objects into a JsonNode with using Jackson’s ObjectMapper.
7. Mappings
Having set up our controller actions, we can now map them, open file student-api/conf/routes and add these routes:
GET / controllers.StudentController.listStudents() POST /:id controllers.StudentController.retrieve(id:Int) POST / controllers.StudentController.create() PUT / controllers.StudentController.update() DELETE /:id controllers.StudentController.delete(id:Int) GET /assets/*file controllers.Assets.versioned( path="/public", file: Asset)
The /assets endpoint must always be present for download of static resources.
After this, we are done with building the student API.
8. Testing
We can now run tests on it by sending requests to http://localhost:9000/ and/or adding the appropriate context. Running the base path from the browser should output:
{ "isSuccessfull":true, "body":[] }
As we can see, the body is empty since we have not added any records yet. Using a Java client, let’s run some tests, we can globalize the base path to avoid repetition:
private static final String BASE_URL = "http://localhost:9000";
Adding records:
@Test public void whenCreatesRecord_thenCorrect() { Student student = new Student("jody", "west", 50); JSONObject obj = new JSONObject(makeRequest( BASE_URL, "POST", new JSONObject(student))); assertTrue(obj.getBoolean("isSuccessfull")); JSONObject body = obj.getJSONObject("body"); assertEquals(student.getAge(), body.getInt("age")); assertEquals(student.getFirstName(), body.getString("firstName")); assertEquals(student.getLastName(), body.getString("lastName")); }
Notice use of makeRequest method, it’s our own helper method to send HTTP requests to any URL:
public static String makeRequest(String myUrl, String httpMethod, JSONObject parameters) { URL url = null; url = new URL(myUrl); HttpURLConnection conn = null; conn = (HttpURLConnection) url.openConnection(); conn.setDoInput(true); conn.setRequestProperty("Content-Type", "application/json"); DataOutputStream dos = null; conn.setRequestMethod(httpMethod); if (Arrays.asList("POST", "PUT").contains(httpMethod)) { String params = parameters.toString(); conn.setDoOutput(true); dos = new DataOutputStream(conn.getOutputStream()); dos.writeBytes(params); dos.flush(); dos.close(); } int respCode = conn.getResponseCode(); if (respCode != 200 && respCode != 201) { String error = inputStreamToString(conn.getErrorStream()); return error; } String inputString = inputStreamToString(conn.getInputStream()); return inputString; }
And also method inputStreamToString:
public static String inputStreamToString(InputStream is) { BufferedReader br = null; StringBuilder sb = new StringBuilder(); String line; br = new BufferedReader(new InputStreamReader(is)); while ((line = br.readLine()) != null) { sb.append(line); } br.close(); return sb.toString(); }
After running the above test, loading http://localhost:9000 from the browser should now give us:
{ "isSuccessfull":true, "body": [{ "firstName":"Barrack", "lastName":"Obama", "age":50, "id":1 }] }
The id attribute will be incrementing for every new record we add.
Deleting a record:
@Test public void whenDeletesCreatedRecord_thenCorrect() { Student student = new Student("Usain", "Bolt", 25); JSONObject ob1 = new JSONObject( makeRequest(BASE_URL, "POST", new JSONObject(student))).getJSONObject("body"); int id = ob1.getInt("id"); JSONObject obj1 = new JSONObject( makeRequest(BASE_URL + "/" + id, "POST", new JSONObject())); assertTrue(obj1.getBoolean("isSuccessfull")); makeRequest(BASE_URL + "/" + id, "DELETE", null); JSONObject obj2 = new JSONObject( makeRequest(BASE_URL + "/" + id, "POST", new JSONObject())); assertFalse(obj2.getBoolean("isSuccessfull")); }
In the above test, we first create a new record, retrieve it successfully by its new id, we then deleted it. When we try to retrieve by the same id again, the operation fails as expected.
Let’s now update the record:
@Test public void whenUpdatesCreatedRecord_thenCorrect() { Student student = new Student("Donald", "Trump", 50); JSONObject body1 = new JSONObject( makeRequest(BASE_URL, "POST", new JSONObject(student))).getJSONObject("body"); assertEquals(student.getAge(), body1.getInt("age")); int newAge = 60; body1.put("age", newAge); JSONObject body2 = new JSONObject( makeRequest(BASE_URL, "PUT", body1)).getJSONObject("body"); assertFalse(student.getAge() == body2.getInt("age")); assertTrue(newAge == body2.getInt("age")); }
The above test demonstrates the change in the value of the age field after updating the record.
Getting all records:
Student student1 = new Student("jane", "daisy", 50); Student student2 = new Student("john", "daniel", 60); Student student3 = new Student("don", "mason", 55); Student student4 = new Student("scarlet", "ohara", 90); makeRequest(BASE_URL, "POST", new JSONObject(student1)); makeRequest(BASE_URL, "POST", new JSONObject(student2)); makeRequest(BASE_URL, "POST", new JSONObject(student3)); makeRequest(BASE_URL, "POST", new JSONObject(student4)); JSONObject objects = new JSONObject(makeRequest(BASE_URL, "GET", null)); assertTrue(objects.getBoolean("isSuccessfull")); JSONArray array = objects.getJSONArray("body"); assertTrue(array.length() >= 4);
With the above test, we are ascertaining the proper functioning of the listStudents controller action.
9. Conclusion
In this article, we have shown how to build a full-fledged REST API using Play Framework.
The full source code and examples for this article are available in the GitHub project.