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:
actionUsername
must represent a valid user in the database.Company
, Project
, or Task
data.Our service layer implementation will use the isValidUser
method in the AbstractService
class to check if the user is valid.
Application security is a critical part of enterprise application development and it is important to understand the difference between authentication and authorization.
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 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.
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.
@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.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.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(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 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:
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 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:
TaskLog
or an administrator can find a task logfindByUser
method requires a valid start and end dateWe 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.
18.117.231.15