Creating the request handlers

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:

Creating the request handlers

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.

Defining the AbstractHandler superclass

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.

Note

Remember that all of the generated JSON objects will be in a format that can be consumed by Ext JS 4 components without the need for additional configuration.

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.

Defining the SecurityHandler class

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 and @RequestMapping annotations

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 method

Any 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.

Note

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.

The @ResponseBody annotation

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.

The @RequestParam annotation

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.

Authenticating a user

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.

Logging out

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..."
}

Defining the CompanyHandler class

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:

  • /company/find will map it to the find method using a GET request
  • /company/store will map it to the store method using a POST request
  • /company/findAll will map it to the findAll method using a GET request
  • /company/remove will map it to the remove method using a POST request

The following are a few things to note:

  • Each handler method is defined with either a 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.
  • Each method retrieves the user from the HTTP session by calling 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.
  • The 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.

Defining the ProjectHandler class

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:

  • /project/find will map to the find method using a GET request
  • /project/store will map to the store method using a POST request
  • /project/findAll will map to the findAll method using a GET request
  • /project/remove will map to the remove method using a POST request

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.239.41