Implementing the service layer

Each interface defined previously will have an appropriate implementation. The implementing classes will follow our DAO naming conventions by adding Impl to the interface names resulting in CompanyServiceImpl, ProjectServiceImpl, TaskServiceImpl, TaskLogServiceImpl, and UserServiceImpl. We will define the CompanyServiceImpl, TaskServiceImpl, and TaskLogServiceImpl classes and leave the ProjectServiceImpl and UserServiceImpl as an exercise.

The service layer implementations will process business logic with one or more calls to the DAO layer, validating parameters, and confirming user authorization as required. The 3T application security is very simple as mentioned in the following list:

  • A valid user is required for all actions. The actionUsername must represent a valid user in the database.
  • Only an administrator can modify the Company, Project, or Task data.
  • Only an administrator can modify or add users.

Our service layer implementation will use the isValidUser method in the AbstractService class to check if the user is valid.

Authentication, authorization, and security

Application security is a critical part of enterprise application development and it is important to understand the difference between authentication and authorization.

  • Authentication verifies who you are. It involves verifying the username/password combination and is performed once during the initial login to the 3T application.
  • Authorization verifies what you are allowed to do. 3T administrators are allowed to perform more actions than normal users.

A 3T user must have a valid record in the ttt_user table; the service layer will simply test if the provided username represents a valid user. The actual authorization of the user will be covered in the next chapter when we develop the request handling layer.

Securing an enterprise application is beyond the scope of this book but no discussion of this topic would be complete without mentioning Spring Security, an overview of which can be found at http://static.springframework.org/spring-security/site/index.html. Spring Security has become the de facto standard for securing Spring-based applications and an excellent book called Spring Security 3, by Packt Publishing, that covers all concepts can be found here at http://www.springsecuritybook.com. We recommend you learn more about Spring Security to understand the many different ways you can authenticate users and secure your service layer.

The CompanyService implementation

The CompanyServiceImpl class is defined as:

package com.gieman.tttracker.service;

import com.gieman.tttracker.dao.CompanyDao;
import java.util.List;
import com.gieman.tttracker.domain.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.gieman.tttracker.vo.Result;
import com.gieman.tttracker.vo.ResultFactory;
import org.springframework.beans.factory.annotation.Autowired;

@Transactional
@Service("companyService")
public class CompanyServiceImpl extends AbstractService implements CompanyService {

    @Autowired
    protected CompanyDao companyDao;

    public CompanyServiceImpl() {
        super();
    }

    @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
    @Override
    public Result<Company> find(Integer idCompany, String actionUsername) {

        if (isValidUser(actionUsername)) {
            Company company = companyDao.find(idCompany);
            return ResultFactory.getSuccessResult(company);

        } else {          
            return ResultFactory.getFailResult(USER_INVALID);
        }
    }

    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    @Override
    public Result<Company> store(
            Integer idCompany,
            String companyName,
            String actionUsername) {

        User actionUser = userDao.find(actionUsername);
        
        if (!actionUser.isAdmin()) {
            return ResultFactory.getFailResult(USER_NOT_ADMIN);
        }

        Company company;

        if (idCompany == null) {
            company = new Company();
        } else {

            company = companyDao.find(idCompany);
            
            if (company == null) {
                return ResultFactory.getFailResult("Unable to find company instance with ID=" + idCompany);
            }
        }

        company.setCompanyName(companyName);

        if (company.getId() == null) {
            companyDao.persist(company);
        } else {
            company = companyDao.merge(company);
        }

        return ResultFactory.getSuccessResult(company);

    }

    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    @Override
    public Result<Company> remove(Integer idCompany, String actionUsername) {

        User actionUser = userDao.find(actionUsername);
        
        if (!actionUser.isAdmin()) {
            return ResultFactory.getFailResult(USER_NOT_ADMIN);
        }

        if (idCompany == null) {
            return ResultFactory.getFailResult("Unable to remove Company [null idCompany]");
        } 

        Company company = companyDao.find(idCompany);

        if (company == null) {
            return ResultFactory.getFailResult("Unable to load Company for removal with idCompany=" + idCompany);
        } else {

            if (company.getProjects() == null || company.getProjects().isEmpty()) {

                companyDao.remove(company);

                String msg = "Company " + company.getCompanyName() + " was deleted by " + actionUsername;
                logger.info(msg);
                return ResultFactory.getSuccessResultMsg(msg);
            } else {
                return ResultFactory.getFailResult("Company has projects assigned and could not be deleted");
            }
        }

    }

    @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
    @Override
    public Result<List<Company>> findAll(String actionUsername) {

        if (isValidUser(actionUsername)) {
            return ResultFactory.getSuccessResult(companyDao.findAll());
        } else {
            return ResultFactory.getFailResult(USER_INVALID);
        }
    }
}

Each method returns a Result object that is created by the appropriate ResultFactory static method. Each method confirms the actionUsername method that identifies a valid user for the action. Methods that modify the Company entity require an administrative user (the store and remove methods). Other methods that retrieve data (the find* method) simply require a valid user; one that exists in the ttt_user table.

Note the reuse of the if(isValidUser(actionUsername)) and if(!actionUser.isAdmin()) code blocks in each method. This is not considered a good practice as this logic should be part of the security framework and not replicated on a per method basis. Using Spring Security, for example, you can apply security to a service layer bean by using annotations.

@Secured("ROLE_USER")
public Result<List<Company>> findAll(String actionUsername) {
// application specific code here

@Secured("ROLE_ADMIN")
public Result<Company> remove(Integer idCompany, String actionUsername) {
// application specific code here

The @Secured annotation is used to define a list of security configuration attributes that are applicable to the business methods. A user would then be linked to one or more roles by the security framework. Such a design pattern is less intrusive, easier to maintain, and easier to enhance.

Note

We once again recommend you learn more about Spring Security for use in real-world enterprise applications.

Any action that cannot be performed as expected is considered to have "failed". In this case, the ResultFactory.getFailResult method is called to create the failure Result object.

A few points to note:

  • Each service layer class uses the @Service annotation to identify this as a Spring-managed bean. The Spring Framework will be configured to scan for this annotation using <context:component-scan base-package="com.gieman.tttracker.service"/> in the application context configuration file. Spring will then load the CompanyServiceImpl class into the bean container under the companyService name.
  • The store method is used to both persist and merge a Company entity. The service layer client has no need to know if this will be an insert statement or an update statement. The appropriate action is selected in the store method based on the existence of the primary key.
  • The remove method checks if the company has projects assigned. The business rule implemented will only allow a company deletion if there are no projects assigned and then check if company.getProjects().isEmpty() is true. If projects are assigned, the remove method fails.
  • Transactional attributes depend on the action being implemented. If data is being modified, we use @Transactional(readOnly = false, propagation = Propagation.REQUIRED) to ensure a transaction is created if not already available. If data is not being modified in the method, we use @Transactional(readOnly = true, propagation = Propagation.SUPPORTS).

All service layer implementations will follow a similar pattern.

The TaskService implementation

The TaskServiceImpl class is defined as follows:

package com.gieman.tttracker.service;

import com.gieman.tttracker.dao.ProjectDao;
import com.gieman.tttracker.dao.TaskDao;
import com.gieman.tttracker.dao.TaskLogDao;
import java.util.List;
import com.gieman.tttracker.domain.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.gieman.tttracker.vo.Result;
import com.gieman.tttracker.vo.ResultFactory;
import org.springframework.beans.factory.annotation.Autowired;

@Transactional
@Service("taskService")
public class TaskServiceImpl extends AbstractService implements TaskService {

    @Autowired
    protected TaskDao taskDao;
    @Autowired
    protected TaskLogDao taskLogDao;     
    @Autowired
    protected ProjectDao projectDao;    
    
    public TaskServiceImpl() {
        super();
    }

    @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
    @Override
    public Result<Task> find(Integer idTask, String actionUsername) {

        if(isValidUser(actionUsername)) {
            return ResultFactory.getSuccessResult(taskDao.find(idTask));
        } else {
            return ResultFactory.getFailResult(USER_INVALID);
        }

    }

    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    @Override
    public Result<Task> store(
        Integer idTask,
        Integer idProject,
        String taskName,
        String actionUsername) {

        User actionUser = userDao.find(actionUsername);
        
        if (!actionUser.isAdmin()) {
            return ResultFactory.getFailResult(USER_NOT_ADMIN);
        }

        Project project = projectDao.find(idProject);
        
        if(project == null){
            return ResultFactory.getFailResult("Unable to store task without a valid project [idProject=" + idProject + "]");
        }

        Task task;

        if (idTask == null) {

            task = new Task();
            task.setProject(project);
            project.getTasks().add(task);

        } else {

            task = taskDao.find(idTask);

            if(task == null) {
                
                return ResultFactory.getFailResult("Unable to find task instance with idTask=" + idTask);
                
            } else {

                if(! task.getProject().equals(project)){

                    Project currentProject = task.getProject();
                    // reassign to new project
                    task.setProject(project);
                    project.getTasks().add(task);
                    // remove from previous project
                    currentProject.getTasks().remove(task);
                }
            }
        }

        task.setTaskName(taskName);

        if(task.getId() == null) {
            taskDao.persist(task);
        } else {
            task = taskDao.merge(task);
        }

        return ResultFactory.getSuccessResult(task);
    }

    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    @Override
    public Result<Task> remove(Integer idTask, String actionUsername){
        User actionUser = userDao.find(actionUsername);
        
        if (!actionUser.isAdmin()) {
            return ResultFactory.getFailResult(USER_NOT_ADMIN);
        }

        if(idTask == null){

            return ResultFactory.getFailResult("Unable to remove Task [null idTask]");

        } else {

            Task task = taskDao.find(idTask);
            long taskLogCount = taskLogDao.findTaskLogCountByTask(task);

            if(task == null) {

                return ResultFactory.getFailResult("Unable to load Task for removal with idTask=" + idTask);

            } else if(taskLogCount > 0) {

                return ResultFactory.getFailResult("Unable to remove Task with idTask=" + idTask + " as valid task logs are assigned");

            } else {

                Project project = task.getProject();

                taskDao.remove(task);

                project.getTasks().remove(task);

                String msg = "Task " + task.getTaskName() + " was deleted by " + actionUsername;
                logger.info(msg);
                return ResultFactory.getSuccessResultMsg(msg);
            }
        }
    }

    @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
    @Override
    public Result<List<Task>> findAll(String actionUsername){

        if(isValidUser(actionUsername)){
            return ResultFactory.getSuccessResult(taskDao.findAll());
        } else {
            return ResultFactory.getFailResult(USER_INVALID);
        }
    }
}

This class implements the following business rules:

  • Removing a task is not allowed if task logs are assigned
  • Only administrators can modify a task

Note that in the remove method we check if task logs are assigned to the task using the code:

long taskLogCount = taskLogDao.findTaskLogCountByTask (task);

The taskLogDao.findTaskLogCountByTask method uses the getSingleResult() method on the Query interface to return a long value as defined in the TaskLogDaoImpl. It would have been possible to code a method as follows to find the taskLogCount:

List<TaskLog> allTasks = taskLogDao.findByTask(task);
long taskLogCount = allTasks.size();

However this option would result in JPA loading all TaskLog entities assigned to the task into memory. This is not an efficient use of resources as there could be millions of TaskLog records in a large system.

The TaskLogService implementation

The TaskLogService implementation will be the final class we will go through in detail.

package com.gieman.tttracker.service;

import com.gieman.tttracker.dao.TaskDao;
import com.gieman.tttracker.dao.TaskLogDao;
import java.util.List;
import com.gieman.tttracker.domain.*;
import java.util.Date;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.gieman.tttracker.vo.Result;
import com.gieman.tttracker.vo.ResultFactory;
import org.springframework.beans.factory.annotation.Autowired;

@Transactional
@Service("taskLogService")
public class TaskLogServiceImpl extends AbstractService implements TaskLogService {

    @Autowired
    protected TaskLogDao taskLogDao;    
    @Autowired
    protected TaskDao taskDao;   
    
    public TaskLogServiceImpl() {
        super();
    }

    @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
    @Override
    public Result<TaskLog> find(Integer idTaskLog, String actionUsername) {

        User actionUser = userDao.find(actionUsername);

        if(actionUser == null) {
            return ResultFactory.getFailResult(USER_INVALID);
        }

        TaskLog taskLog = taskLogDao.find(idTaskLog);

        if(taskLog == null){
            return ResultFactory.getFailResult("Task log not found with idTaskLog=" + idTaskLog);
        } else if( actionUser.isAdmin() || taskLog.getUser().equals(actionUser)){
            return ResultFactory.getSuccessResult(taskLog);
        } else {
            return ResultFactory.getFailResult("User does not have permission to view this task log");
        }
    }

    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    @Override
    public Result<TaskLog> store(
        Integer idTaskLog,
        Integer idTask,
        String username,
        String taskDescription,
        Date taskLogDate,
        int taskMinutes,
        String actionUsername) {

        User actionUser = userDao.find(actionUsername);
        User taskUser = userDao.find(username);

        if(actionUser == null || taskUser == null) {
            return ResultFactory.getFailResult(USER_INVALID);
        }

        Task task = taskDao.find(idTask);

        if(task == null) {
            return ResultFactory.getFailResult("Unable to store task log with null task");
        }

        if( !actionUser.isAdmin() && ! taskUser.equals(actionUser) ){
            return ResultFactory.getFailResult("User performing save must be an admin user or saving their own record");
        }

        TaskLog taskLog;

        if (idTaskLog == null) {
            taskLog = new TaskLog();
        } else {
            taskLog = taskLogDao.find(idTaskLog);
            if(taskLog == null) {
                return ResultFactory.getFailResult("Unable to find taskLog instance with ID=" + idTaskLog);
            }
        }

        taskLog.setTaskDescription(taskDescription);
        taskLog.setTaskLogDate(taskLogDate);
        taskLog.setTaskMinutes(taskMinutes);
        taskLog.setTask(task);
        taskLog.setUser(taskUser);

        if(taskLog.getId() == null) {
            taskLogDao.persist(taskLog);
        } else {
            taskLog = taskLogDao.merge(taskLog);
        }

        return ResultFactory.getSuccessResult(taskLog);

    }

    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    @Override
    public Result<TaskLog> remove(Integer idTaskLog, String actionUsername){

        User actionUser = userDao.find(actionUsername);

        if(actionUser == null) {
            return ResultFactory.getFailResult(USER_INVALID);
        }

        if(idTaskLog == null){
            return ResultFactory.getFailResult("Unable to remove TaskLog [null idTaskLog]");
        } 

        TaskLog taskLog = taskLogDao.find(idTaskLog);

        if(taskLog == null) {
            return ResultFactory.getFailResult("Unable to load TaskLog for removal with idTaskLog=" + idTaskLog);
        } 

        // only the user that owns the task log may remove it
        // OR an admin user
        if(actionUser.isAdmin() || taskLog.getUser().equals(actionUser)){
            taskLogDao.remove(taskLog);
            return ResultFactory.getSuccessResultMsg("taskLog removed successfully");
        } else {
            return ResultFactory.getFailResult("Only an admin user or task log owner can delete a task log");
        }
    }

    @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
    @Override
    public Result<List<TaskLog>> findByUser(String username, Date startDate, Date endDate, String actionUsername){

        User taskUser = userDao.findByUsername(username);
        User actionUser = userDao.find(actionUsername);

        if(taskUser == null || actionUser == null) {
            return ResultFactory.getFailResult(USER_INVALID);
        }

        if(startDate == null || endDate == null){
            return ResultFactory.getFailResult("Start and end date are required for findByUser ");
        }
        
        if(actionUser.isAdmin() || taskUser.equals(actionUser)){
            return ResultFactory.getSuccessResult(taskLogDao.findByUser(taskUser, startDate, endDate));
        } else {
            return ResultFactory.getFailResult("Unable to find task logs. User does not have permission with username=" + username);
        }
    }
}

Once again there is a lot of business logic in this class. The main business rules implemented are:

  • Only the owner of the TaskLog or an administrator can find a task log
  • An administrator can add a task log for any other user
  • A normal user can only add a task log for themselves
  • Only the owner of a task log or an administrator can remove a task log
  • A normal user can only retrieve their own task logs
  • An administrator can retrieve anyone's task logs
  • The findByUser method requires a valid start and end date

We leave the remaining service layer classes (UserServiceImpl and ProjectServiceImpl) for you to implement as exercises.

It is now time to configure the testing environment for our service layer.

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

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