We will now build the handlers that are used to serve the HTTP requests from our Ext JS client. These handlers will be added to a new web
directory, as shown in the following screenshot:
Each handler will use the Spring Framework @Controller
annotation to indicate that the class serves the role of a "controller". Strictly speaking, the handlers that we will be defining are not controllers in the traditional sense of a Spring MVC application. We will only be using a very small portion of the available Spring controller functionality to process requests. This will ensure that our request handling layer is very lightweight and easy to maintain. As always, we will start by creating a base class that all the handlers will implement.
The AbstractHandler
superclass defines several important methods that are used to simplify JSON generation. As we are working toward integration with Ext JS 4 clients, the structure of the JSON object generated by our handlers is specific to data structures expected by Ext JS 4 components. We will always generate a JSON object with a success
property that holds a Boolean true
or false
value. Likewise, we will always generate a JSON object with a payload property named data
. This data
property will have a valid JSON object as its value, either as a simple JSON object or as a JSON array.
The definition of the AbstractHandler
class is as follows:
package com.gieman.tttracker.web; import com.gieman.tttracker.domain.JsonItem; import java.io.StringReader; import java.io.StringWriter; import java.util.List; import javax.json.Json; import javax.json.JsonArrayBuilder; import javax.json.JsonNumber; import javax.json.JsonObject; import javax.json.JsonObjectBuilder; import javax.json.JsonReader; import javax.json.JsonValue; import javax.json.JsonWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class AbstractHandler { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); public static String getJsonSuccessData(List<? extends JsonItem> results) { final JsonObjectBuilder builder = Json.createObjectBuilder(); builder.add("success", true); final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder(); for (JsonItem ji : results) { arrayBuilder.add(ji.toJson()); } builder.add("data", arrayBuilder); return toJsonString(builder.build()); } public static String getJsonSuccessData(JsonItem jsonItem) { final JsonObjectBuilder builder = Json.createObjectBuilder(); builder.add("success", true); builder.add("data", jsonItem.toJson()); return toJsonString(builder.build()); } public static String getJsonSuccessData(JsonItem jsonItem, int totalCount) { final JsonObjectBuilder builder = Json.createObjectBuilder(); builder.add("success", true); builder.add("total", totalCount); builder.add("data", jsonItem.toJson()); return toJsonString(builder.build()); } public static String getJsonErrorMsg(String theErrorMessage) { return getJsonMsg(theErrorMessage, false); } public static String getJsonSuccessMsg(String msg) { return getJsonMsg(msg, true); } public static String getJsonMsg(String msg, boolean success) { final JsonObjectBuilder builder = Json.createObjectBuilder(); builder.add("success", success); builder.add("msg", msg); return toJsonString(builder.build()); } public static String toJsonString(JsonObject model) { final StringWriter stWriter = new StringWriter(); try (JsonWriter jsonWriter = Json.createWriter(stWriter)) { jsonWriter.writeObject(model); } return stWriter.toString(); } protected JsonObject parseJsonObject(String jsonString) { JsonReader reader = Json.createReader(new StringReader(jsonString)); return reader.readObject(); } protected Integer getIntegerValue(JsonValue jsonValue) { Integer value = null; switch (jsonValue.getValueType()) { case NUMBER: JsonNumber num = (JsonNumber) jsonValue; value = num.intValue(); break; case NULL: break; } return value; } }
The overloaded getJsonSuccessData
methods will each generate a JSON string with the success
property set to true
and an appropriate data
JSON payload. The getJsonXXXMsg
variants will also generate a JSON String with an appropriate success
property (either true
for a successful action or false
for a failed action) and an msg
property that holds the appropriate message for consumption by the Ext JS component.
The parseJsonObject
method will parse a JSON string into a JsonObject
using the JsonReader
instance. The toJsonString
method will write a JsonObject
to its JSON string representation using the JsonWriter
instance. These classes are part of the Java EE 7 javax.json
package, and they make working with JSON very easy.
The getIntegerValue
method is used to parse a JsonValue
object into an Integer
type. A JsonValue
object may be of several different types as defined by the javax.json.jsonValue.ValueType
constants, and appropriate checks are performed on the value prior to attempting to parse the JsonValue
object into an Integer
. This will allow us to send JSON data from Ext JS clients in the following form:
{
success: true,
data: {
"idCompany":null,
"companyName": "New Company"
}
}
Note that the idCompany
property has a value of null
. The getIntegerValue
method allows us to parse integers that may be null
, something that is not possible when using the default JsonObject.getInt(key)
method (which throws an exception if a null
value is encountered).
Let's now define our first handler class that will process user authentication.
We first define a simple helper class that can be used to verify whether a user session is active:
package com.gieman.tttracker.web; import com.gieman.tttracker.domain.User; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; public class SecurityHelper { static final String SESSION_ATTRIB_USER = "sessionuser"; public static User getSessionUser(HttpServletRequest request) { User user = null; HttpSession session = request.getSession(true); Object obj = session.getAttribute(SESSION_ATTRIB_USER); if (obj != null && obj instanceof User) { user = (User) obj; } return user; } }
The static constant SESSION_ATTRIB_USER
will be used as the name of the session property that holds the authenticated user. All handler classes will call the SecurityHelper.getSessionUser
method to retrieve the authenticated user
from the session. A user session may time out due to inactivity, and the HTTP session will then be removed by the application server. When this happens, the SecurityHelper.getSessionUser
method will return null
, and the 3T application must handle this gracefully.
The SecurityHandler
class is used to authenticate the user credentials. If a user is successfully authenticated, the user
object is stored in the HTTP session using the SESSION_ATTRIB_USER
attribute. It is also possible for the user to log out of the 3T application by clicking on the Log Out button. In this case the user is removed from the session.
The verification and logout functionalities are implemented as follows:
package com.gieman.tttracker.web; import com.gieman.tttracker.domain.User; import com.gieman.tttracker.service.UserService; import com.gieman.tttracker.vo.Result; import static com.gieman.tttracker.web.AbstractHandler.getJsonErrorMsg; import static com.gieman.tttracker.web.SecurityHelper.SESSION_ATTRIB_USER; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @Controller @RequestMapping("/security") public class SecurityHandler extends AbstractHandler { @Autowired protected UserService userService; @RequestMapping(value = "/logon", method = RequestMethod.POST, produces = {"application/json"}) @ResponseBody public String logon( @RequestParam(value = "username", required = true) String username, @RequestParam(value = "password", required = true) String password, HttpServletRequest request) { Result<User> ar = userService.findByUsernamePassword(username, password); if (ar.isSuccess()) { User user = ar.getData(); HttpSession session = request.getSession(true); session.setAttribute(SESSION_ATTRIB_USER, user); return getJsonSuccessData(user); } else { return getJsonErrorMsg(ar.getMsg()); } } @RequestMapping(value = "/logout", produces = {"application/json"}) @ResponseBody public String logout(HttpServletRequest request) { HttpSession session = request.getSession(true); session.removeAttribute(SESSION_ATTRIB_USER); return getJsonSuccessMsg("User logged out..."); } }
The
SecurityHandler
class introduces many new Spring annotations and concepts that need to be explained in detail.
The @Controller
annotation indicates that this class serves the role of a Spring controller. The @Controller
annotated classes are autodetected by Spring component scanning, the configuration of which is defined later in this chapter. But what exactly is a controller?
A Spring controller is part of the Spring MVC framework and usually acts with models and views to process requests. We have no need for either models or views; in fact, our processing lifecycle is managed entirely by the controller itself. Each controller is responsible for a URL mapping as defined in the class-level @RequestMapping
annotation. This mapping maps a URL path to the controller. In our 3T application, any URL starting with /security/
will be directed to the SecurityHandler
class for further processing. Any subpath will then be used to match a method-level @RequestMapping
annotation. We have two methods defined, each with their own unique mapping. This results in the following URL path-to-method mappings:
/security/logon
will map to the logon
method/security/logout
will map to the logout
methodAny other URL starting with /security/
will not match the defined methods and would produce a 404
error.
The name of the method is not important; it is the @RequestMapping
annotation that defines the method used to serve a request.
There are two additional properties defined in the logon
@RequestMapping
annotation. The method=RequestMethod.POST
property specifies that the logon request URL /security/logon
must be submitted as a POST
request. If any other request type was used for the /security/logon
submission, a 404
error would be returned. Ext JS 4 stores and models using AJAX will submit POST
requests by default. Actions that read data, however, will be submitted using a GET
request unless configured otherwise. The other possible methods used in RESTful web services include PUT
and DELETE
, but we will only define the GET
and POST
requests in our application.
It is considered a best practice to ensure that each @RequestMapping
method has an appropriate RequestMethod
defined. The actions that modify data should always be submitted using a POST
request. The actions that hold sensitive data (for example, passwords) should also be submitted using a POST
request to ensure that the data is not sent in a URL-encoded format. The read actions may be sent as either a GET
or a POST
request depending on your application needs.
The produces = {"application/json"}
property defines the producible media types of the mapped request. All of our requests will produce JSON data that has the media type application/json
. Each HTTP request submitted by a browser has an Accept
header, such as:
text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
If the Accept
request does not include the produces
property media type, then the following 406 Not Acceptable
error is returned by the GlassFish 4 server:
The resource identified by this request is only capable of generating responses with characteristics not acceptable according to the request "accept" headers.
All modern browsers will accept the application/json
content type.
This annotation is used by Spring to identify the methods that should return the content directly to the HTTP response output stream (not placed in a model or interpreted as a view name, which is the default Spring MVC behavior). How this is achieved will depend on the return type of the method. All of our request handling methods will return Java Strings, and Spring will internally use a StringHttpMessageConverter
instance to write the String to the HTTP response output stream with a Content-Type
of value text/plain
. This is a very easy way of returning JSON data object String to an HTTP client and thus makes request handling a trivial process.
This annotation on a method argument maps a request parameter to the argument itself. In the logon
method we have the following definition:
@RequestParam(value = "username", required = true) String username, @RequestParam(value = "password", required = true) String password,
Assuming that the logon
method was of the type GET
(it is set to POST
in the SecurityHandler
class, and hence the following URL encoding would not work), a URL such as the following would call the method with a username
value of bjones
and a password
value of admin
:
/security/logon.json?username=bjones&password=admin
We could just as easily have written this method with the following definition:
@RequestParam(value = "user", required = true) String username, @RequestParam(value = "pwd", required = true) String password,
This would then map a URL of the following form:
/security/logon.json?user=bjones&pwd=admin
Note that it is the value
property of the @RequestParam
annotation that maps to the request parameter name.
The required
property of the @RequestParam
annotation defines if this parameter is a required field. The following URL would result in an exception:
/security/logon.json?username=bjones
The password parameter is obviously missing, which does not adhere to the required=true
definition.
Note that the required=true
property only checks for the existence of a request parameter that matches the value
of the @RequestParam
annotation. It is entirely valid to have a request parameter that is empty. The following URL would not throw an exception:
/security/logon.json?username=bjones&password=
Optional parameters may be defined by using the required=false
property and may also include a defaultValue
. Consider the following method argument:
@RequestParam(value = "address", required = false, defaultValue = "Unknown address") String address
Also consider the following three URLs:
/user/address.json?address=Melbourne
/user/address.json?address=
/user/address.json?
The first URL will result in an address value Melbourne
, the second URL will have a null address, and the third URL will have an "unknown address". Note that the defaultValue
will only be used if the request does not have a valid address parameter, and not if the address parameter is empty.
The logon
method in our SecurityHandler
class is very simple thanks to our implementation of the service-layer business logic. We call the userService.findByUsernamePassword(username, password)
method and check the returned Result
. If the Result
is successful, the SecurityHandler.logon
method will return a JSON representation of the authenticated user. This is achieved by the line getJsonSuccessData(user)
, which will result in the following output being written to the HTTP response:
{ "success": true, "data": { "username": "bjones", "firstName": "Betty", "lastName": "Jones", "email": "[email protected]", "adminRole": "Y", "fullName": "Betty Jones" } }
Note that the preceding formatting is for readability only. The actual response will be a stream of characters. The authenticated user is then added to the HTTP session with the attribute SESSION_ATTRIB_USER
. We are then able to identify the authenticated user by calling SecurityHelper.getSessionUser(request)
in our request handlers.
A Result
instance that has failed will call the getJsonErrorMsg(ar.getMsg())
method, which will result in the following JSON object being returned in the HTTP response:
{ "success": false, "msg": "Unable to verify user/password combination!" }
The msg
text is set on the Result
instance in the UserServiceImpl.findByUsernamePassword
method. The Ext JS frontend will process each result differently depending on the success
property.
The logic in this method is very simple: remove the user from the session and return a successful JSON message. The Ext JS frontend will then take an appropriate action. There is no RequestMethod
defined in the @RequestMapping
annotation as no data is being sent. This means that any RequestMethod
may be used to map this URL (GET
, POST
, and so on). The JSON object returned from this method is as follows:
{ "success": true, "msg": "User logged out..." }
This handler processes company actions and is mapped to the /company/
URL pattern.
package com.gieman.tttracker.web;
import com.gieman.tttracker.domain.*;
import com.gieman.tttracker.service.CompanyService;
import com.gieman.tttracker.service.ProjectService;
import com.gieman.tttracker.vo.Result;
import static com.gieman.tttracker.web.SecurityHelper.getSessionUser;
import java.util.List;
import javax.json.JsonObject;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping("/company")
public class CompanyHandler extends AbstractHandler {
@Autowired
protected CompanyService companyService;
@Autowired
protected ProjectService projectService;
@RequestMapping(value = "/find", method = RequestMethod.GET, produces = {"application/json"})
@ResponseBody
public String find(
@RequestParam(value = "idCompany", required = true) Integer idCompany,
HttpServletRequest request) {
User sessionUser = getSessionUser(request);
if (sessionUser == null) {
return getJsonErrorMsg("User is not logged on");
}
Result<Company> ar = companyService.find(idCompany, sessionUser.getUsername());
if (ar.isSuccess()) {
return getJsonSuccessData(ar.getData());
} else {
return getJsonErrorMsg(ar.getMsg());
}
}
@RequestMapping(value = "/store", method = RequestMethod.POST, produces = {"application/json"})
@ResponseBody
public String store(
@RequestParam(value = "data", required = true) String jsonData,
HttpServletRequest request) {
User sessionUser = getSessionUser(request);
if (sessionUser == null) {
return getJsonErrorMsg("User is not logged on");
}
JsonObject jsonObj = parseJsonObject(jsonData);
Result<Company> ar = companyService.store(
getIntegerValue(jsonObj.get("idCompany")),
jsonObj.getString("companyName"),
sessionUser.getUsername());
if (ar.isSuccess()) {
return getJsonSuccessData(ar.getData());
} else {
return getJsonErrorMsg(ar.getMsg());
}
}
@RequestMapping(value = "/findAll", method = RequestMethod.GET, produces = {"application/json"})
@ResponseBody
public String findAll(HttpServletRequest request) {
User sessionUser = getSessionUser(request);
if (sessionUser == null) {
return getJsonErrorMsg("User is not logged on");
}
Result<List<Company>> ar = companyService.findAll(sessionUser.getUsername());
if (ar.isSuccess()) {
return getJsonSuccessData(ar.getData());
} else {
return getJsonErrorMsg(ar.getMsg());
}
}
@RequestMapping(value = "/remove", method = RequestMethod.POST, produces = {"application/json"})
@ResponseBody
public String remove(
@RequestParam(value = "data", required = true) String jsonData,
HttpServletRequest request) {
User sessionUser = getSessionUser(request);
if (sessionUser == null) {
return getJsonErrorMsg("User is not logged on");
}
JsonObject jsonObj = parseJsonObject(jsonData);
Result<Company> ar = companyService.remove(
getIntegerValue(jsonObj.get("idCompany")),
sessionUser.getUsername());
if (ar.isSuccess()) {
return getJsonSuccessMsg(ar.getMsg());
} else {
return getJsonErrorMsg(ar.getMsg());
}
}
}
Each method is mapped to a different sub URL as defined by the method-level @RequestMapping
annotation. The CompanyHandler
class will hence be mapped to the following URLs:
The following are a few things to note:
RequestMethod.POST
or RequestMethod.GET
. The GET
method is used for finder methods, and the POST
method is used for data-modifying methods. These method types are the defaults used by Ext JS for each action.getSessionUser(request)
and then tests if the user
value is null
. If the user is not in session, the message "User is not logged on"
is returned in the JSON-encoded HTTP response.POST
methods have a single request parameter that holds the JSON data submitted by the Ext JS client. This JSON string is then parsed into a JsonObject
before calling the appropriate service layer method using the required parameters.A typical JSON data payload for adding a new company would look like the following:
{"idCompany":null,"companyName":"New Company"}
Note that the idCompany
value is null
. If you are modifying an existing company record, the JSON data payload must contain a valid idCompany
value:
{"idCompany":5,"companyName":"Existing Company"}
Note also that the JSON data holds exactly one company record. It is possible to configure Ext JS clients to submit multiple records per request by submitting a JSON array similar to the following array:
[ {"idCompany":5,"companyName":"Existing Company"}, {"idCompany":4,"companyName":"Another Existing Company"} ]
However, we will restrict our logic to processing a single record per request.
The ProjectHandler
class processes the project actions and is mapped to the /project/
URL pattern as follows:
package com.gieman.tttracker.web; import com.gieman.tttracker.domain.*; import com.gieman.tttracker.service.ProjectService; import com.gieman.tttracker.vo.Result; import static com.gieman.tttracker.web.SecurityHelper.getSessionUser; import java.util.List; import javax.json.JsonObject; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RequestParam; @Controller @RequestMapping("/project") public class ProjectHandler extends AbstractHandler { @Autowired protected ProjectService projectService; @RequestMapping(value = "/find", method = RequestMethod.GET, produces = {"application/json"}) @ResponseBody public String find( @RequestParam(value = "idProject", required = true) Integer idProject, HttpServletRequest request) { User sessionUser = getSessionUser(request); if (sessionUser == null) { return getJsonErrorMsg("User is not logged on"); } Result<Project> ar = projectService.find(idProject, sessionUser.getUsername()); if (ar.isSuccess()) { return getJsonSuccessData(ar.getData()); } else { return getJsonErrorMsg(ar.getMsg()); } } @RequestMapping(value = "/store", method = RequestMethod.POST, produces = {"application/json"}) @ResponseBody public String store( @RequestParam(value = "data", required = true) String jsonData, HttpServletRequest request) { User sessionUser = getSessionUser(request); if (sessionUser == null) { return getJsonErrorMsg("User is not logged on"); } JsonObject jsonObj = parseJsonObject(jsonData); Result<Project> ar = projectService.store( getIntegerValue(jsonObj.get("idProject")), getIntegerValue(jsonObj.get("idCompany")), jsonObj.getString("projectName"), sessionUser.getUsername()); if (ar.isSuccess()) { return getJsonSuccessData(ar.getData()); } else { return getJsonErrorMsg(ar.getMsg()); } } @RequestMapping(value = "/remove", method = RequestMethod.POST, produces = {"application/json"}) @ResponseBody public String remove( @RequestParam(value = "data", required = true) String jsonData, HttpServletRequest request) { User sessionUser = getSessionUser(request); if (sessionUser == null) { return getJsonErrorMsg("User is not logged on"); } JsonObject jsonObj = parseJsonObject(jsonData); Result<Project> ar = projectService.remove( getIntegerValue(jsonObj.get("idProject")), sessionUser.getUsername()); if (ar.isSuccess()) { return getJsonSuccessMsg(ar.getMsg()); } else { return getJsonErrorMsg(ar.getMsg()); } } @RequestMapping(value = "/findAll", method = RequestMethod.GET, produces = {"application/json"}) @ResponseBody public String findAll( HttpServletRequest request) { User sessionUser = getSessionUser(request); if (sessionUser == null) { return getJsonErrorMsg("User is not logged on"); } Result<List<Project>> ar = projectService.findAll(sessionUser.getUsername()); if (ar.isSuccess()) { return getJsonSuccessData(ar.getData()); } else { return getJsonErrorMsg(ar.getMsg()); } } }
The ProjectHandler
class will hence be mapped to the following URLs:
Note that in the store
method, we are once again retrieving the required data from the parsed JsonObject
. The structure of the JSON data
payload when adding a new project is in the following format:
{"idProject":null,"projectName":"New Project","idCompany":1}
When updating an existing project, the JSON structure is as follows:
{"idProject":7,"projectName":"Existing Project with ID=7","idCompany":1}
You will also notice that we once again have the same block of code replicated in each method, as we did in the CompanyHandler
class:
if (sessionUser == null) { return getJsonErrorMsg("User is not logged on"); }
Every method in each of the remaining handlers will also require the same check; a user must be in session to perform the action. This is precisely why we will simplify our code by introducing the concept of Spring request handler interceptors.
3.139.239.41