Book Store - CDI Services - JBoss Weld CDI for Java Platform (2013)

JBoss Weld CDI for Java Platform (2013)

Chapter 9. Book Store - CDI Services

This chapter will explore creating parts of a real-life application with CDI to provide a more indicative picture of how it can be utilized. We will also cover some of the CDI usage patterns that can be beneficial for us to include in our applications as they are developed. Parts of the application will provide clear examples of topics that we have discussed in the previous chapters, while providing a means to see how a particular topic that we covered can be integrated into an application.

Overview of the application

The application that we will build with CDI is one that we will all be familiar with, that of an online book store! Though we don't intend to develop the application to the level of complexity that is provided by Amazon.com, it will provide a good overview of using CDI within an application for us to explore various aspects of its development.

The entities our application will require are as follows:

· User: This entity will hold the user login credentials, and is applicable to customers and internal site administrators.

· Account: This entity is used for capturing additional information that only applies to customers, such as addresses, and it will be the link to any orders that a customer may place.

· Category: This entity defines a grouping that a book is part of.

· Author: This entity specifies an author within the application that can be linked to a book.

· Book: This entity captures all the information about a particular book, and links to the category and author associated with it.

· Order: This entity captures all the information about an order placed by a customer account. It will link to one or more OrderItem entities that will capture which book is being ordered and in what quantity.

Adding interceptors for our services

Before we begin developing our services, as part of the design process, we decided that we wanted to restrict access to some methods based on the User role and that some methods would require a Transaction object to be present.

Securing methods with an interceptor

To be able to develop an interceptor that we can use in our services, there are a few pieces that must be created, as shown in the following list:

1. We need to define an enum function for the possible roles using the following code:

2. public enum RoleType {

3. GUEST,

4. USER,

5. ORDER_PROCESSOR,

6. ADMIN;

}

7. We also need an annotation that we can add to methods to inform CDI that we want them to be intercepted:

8. @InterceptorBinding

9.

10.@Target( { TYPE, METHOD } )

11.

12.@Retention( RUNTIME )

13.public @interface Secure {

14.

15. @Nonbinding

16. RoleType[] rolesAllowed() default {};

}

Note

We specified the RoleType as @Nonbinding as its value is only important to the interceptor implementation and nothing else.

Within our interceptor, we need to know whether there is currently a user logged into the application, and what his/her RoleType is; otherwise, we will not be able to determine whether we should grant him/her access to the method!

17. We will create a CDI bean that we can use to hold the information about the current user, as follows:

18.public class CurrentUser implements Serializable {

19. private boolean loggedIn = false;

20. private Long userId;

21. private String name;

22.

23. private String email;

24. private RoleType roleType = RoleType.GUEST;

25. public CurrentUser() {

26. }

27. public CurrentUser(Long id, String name, String email,RoleType roleType) {

28. this.userId = id;

29. this.name = name;

30. this.email = email;

31. this.loggedIn = true;

32. this.roleType = roleType;

33. }

34. public Long getUserId() {

35. return userId;

36. }

37. public String getName() {

38. return name;

39. }

40. public String getEmail() {

41. return email;

42. }

43. public boolean isLoggedIn() {

44. return loggedIn;

45. }

46. public RoleType getRoleType() {

47. return roleType;

48. }

}

We've made CurrentUser implement Serializable as we will be injecting this bean into a @SessionScoped bean later.

Note

At present, this bean will always return false when isLoggedIn() is called, and GUEST when getRoleType() is called. This is intentional as when we discuss services later, we will discover how we can update them.

49. To make it easier to distinguish between possible instances of CurrentUser within our application, we will introduce a qualifier that we can use:

50.@Qualifier

51.@Target( { FIELD, PARAMETER, METHOD } )

52.@Retention( RUNTIME )

public @interface LoggedIn {}

53. Now that we have all the pieces, we can create our interceptor for securing our service methods as follows:

54.@Secure

55.

56.@Interceptor

57.public class SecurityInterceptor {

58.

59. @Inject

60.

61. @LoggedIn

62. Instance<CurrentUser> currentUserInstance;

63.

64. @AroundInvoke

65. public Object checkRoles(InvocationContext context) throws Exception {

66. // Check for defined roles

67. Secure secure = getAnnotation(context.getMethod());

68. RoleType[] roles = secure.rolesAllowed();

69. if (roles.length == 0) {

70. throwException("No RoleType's defined for @Secure: ", context.getMethod());

71. }

72. boolean roleMatches = false;

73. for (int i = 0; i < roles.length; i++) {

74. if (roles[i].equals(currentUserInstance.get().getRoleType())) {

75. roleMatches = true;

76. break;

77. }

78. }

79. if (!roleMatches) {

80. throwException("User does not have permission to call method: ", context.getMethod());

81. }

82. return context.proceed();

83. }

84....

}

Note

We injected Instance<CurrentUser> instead of CurrentUser to ensure that at the point the interceptor is called, we are retrieving the most recent CurrentUser, instead of the one that was present when the interceptor was created, which could possibly be different.

Inside checkRoles(), we retrieve the @Secure annotation from the method or class depending on where it was defined, to know what roles are allowed for this method call. If the RoleType value on CurrentUser matches one of the allowed roles, then we callcontext.proceed() ; otherwise we throw an AuthorizationException.

Providing a transaction with an interceptor

We also need an interceptor for ensuring that particular service calls are present within a UserTransaction of the container. We will use the following steps to do so:

1. The annotation for the transaction doesn't require any values, so it is just as follows:

2. @InterceptorBinding

3.

4. @Target( { TYPE, METHOD } )

5.

6. @Retention( RUNTIME )

public @interface Transactional {}

7. And our interceptor utilizes an injected UserTransaction to determine whether we're already associated with a transaction, or we need to begin one as shown in the following code snippet:

8. @Transactional

9.

10.@Interceptor

11.public class TransactionInterceptor {

12.

13. @Resource

14. UserTransaction userTrans;

15.

16. @AroundInvoke

17. public Object manageTransaction(InvocationContext context) throws Exception {

18. boolean transactionPresent = false;

19. if (userTrans.getStatus() == Status.STATUS_NO_TRANSACTION) {

20. userTrans.begin();

21. } else {

22. transactionPresent = true;

23. }

24. Object result;

25. try {

26. result = context.proceed();

27. } catch (Exception e) {

28. userTrans.rollback();

29. throw e;

30. }

31. if (userTrans.getStatus() == Status.STATUS_MARKED_ROLLBACK) {

32. userTrans.rollback();

33. } else if (userTrans.getStatus() != Status.STATUS_ROLLEDBACK) {

34. if (!transactionPresent) {

35. userTrans.commit();

36. }

37. }

38. return result;

39. }

}

40. To activate our two interceptors within the application, we need to include them into the beans.xml file of our archive, which is located in META-INF of our jar:

41.<beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

42. xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">

43. <interceptors>

44. <class>

45. org.cdibook.chapter9.interceptor.SecurityInterceptor

46. </class>

47. <class>

48. org.cdibook.chapter9.interceptor.TransactionInterceptor

49. </class>

50. </interceptors>

</beans>

We deliberately chose to list SecurityInterceptor before TransactionInterceptor; the order in beans.xml determines the order in which they are executed, as there is no need to start a transaction if the caller does not have the necessary permissions to execute the method.

Creating CDI services

In creating the services that interact with JPA, we decided to abstract some methods out into a parent service that all our services can inherit. It wasn't necessary to develop it in this way, as it is all down to personal preference in the end.

1. Our parent service will inject an EntityManager for JPA calls, and will take advantage of Java Generics to provide some common methods for retrieving an entity by an identifier and retrieving all instances of an entity type:

2. public abstract class AbstractService<T> {

3.

4. @Inject

5.

6. @BookDB

7. private EntityManager entityManager;

8. private Class<T> entityClass;

9. public AbstractService() {}

10. public AbstractService(Class<T> entityClass) {

11. this.entityClass = entityClass;

12. }

13. protected EntityManager getEntityManager() {

14. return entityManager;

15. }

16.

17. @Transactional

18. public T get(Long id) throws ServiceException {

19. ...

20. }

21.

22. @Transactional

23. public List<T> getAll(Map<String, String> parameters) {

24. ...

25. }

26. protected Predicate[] buildPredicates(Map<String, String> params, CriteriaBuilder criteriaBuilder, Root<T> root) {

27. return new Predicate[]{};

28. }

}

We use @Transactional on get() and getAll() to indicate that we wish those methods to be called with an active transaction, as the annotation will trigger our interceptor to be called, but only when it has been defined within beans.xml, as we did earlier.

Note

The implementation of get() and getAll() utilize JPA criteria-building APIs, and can be seen within the code of the chapter.

29. Now that we've created the abstract service, let's create a service to manage User data:

30.@RequestScoped

31.public class UserService extends AbstractService<User> {

32. ...

33. @Transactional

34.

35. @Secure(rolesAllowed = {RoleType.GUEST, RoleType.ADMIN})

36. public void createUser(User user) throws ServiceException {

37. if (null != user.getId()) {

38. throw new EntityExistsException();

39. }

40. try {

41. getEntityManager().persist(user);

42. } catch (ConstraintViolationException cve) {

43. throw new ServiceException(cve);

44. }

45. }

46.

47. @Secure(rolesAllowed = RoleType.GUEST)

48. public void login(String email, String password) throws ServiceException {

49. if (currentUserInstance.get().isLoggedIn()) {

50. // Already logged in

51. return;

52. }

53. Map<String, String> params = new HashMap<>();

54. params.put("email", email);

55. List<User> results = getAll(params);

56. if (null == results||results.size() == 0||results.size() > 1) {

57. throw new ServiceException("User record not found.");

58. }

59. User user = results.get(0);

60. if (!user.passwordMatches(password)) {

61. throw new ServiceException("Unable to login user.");

62. }

63. userLoggedInEvent.fire(user);

64. }

65....

}

Currently we've defined UserService as @RequestScoped, as there is no reason to make it live for longer than that with the current design. We've added @Transactional and @Secure onto our methods, restricting login() to only be called when the RoleType value isGUEST and createUser() is restricted to GUEST and ADMIN. It might seem unusual for createUser() to be restricted in such a way, but it does make sense for our application as we want to restrict the ability to create a user in the application to new customers and administrators.

Note

The ConstraintViolationException is wrapped into our application's ServiceException because it is not Serializable, which prevents it from being used as a test exception scenario with Arquillian—the integration testing framework from JBoss.

66. As part of login(), we fire a CDI event to inform any part of our application that is listening that there is now a non GUEST user accessing the application. As shown in the following code, we need to provide a means for a CurrentUser entity to be produced by our application that does not contain the default values, as we saw earlier:

67.@SessionScoped

68.public class Authentication implements Serializable {

69.private CurrentUser currentUser;

70. public void userLoggedIn(

71.

72. @Observes(notifyObserver = Reception.IF_EXISTS)

73. @LoggedIn User loggedInUser) {

74.

75. currentUser = new CurrentUser(loggedInUser.getId(),

76. loggedInUser.getName(),

77. loggedInUser.getEmail(),

78. loggedInUser.getRoleType());

79. }

80. public void userLoggedOut(

81.

82. @Observes(notifyObserver = Reception.IF_EXISTS)

83. @LoggedOut CurrentUser currentUser) {

84. if (null != this.currentUser

85. && this.currentUser.equals(currentUser)) {

86. this.currentUser = null;

87. }

88. }

89.

90. @Produces

91.

92. @Named("currentUser")

93.

94. @LoggedIn

95. public CurrentUser produceCurrentUser() {

96. return null != this.currentUser ? this.currentUser : new CurrentUser();

97. }

}

Our @SessionScoped bean stores a non CDI managed instance of CurrentUser, which is created when it observes the @LoggedIn event from UserService.login() . For any other part of our application that needs to inject CurrentUser, our bean defines a producer that provides a @Dependent scoped instance to whichever InjectionPoint requires it. Though the produced bean is cleaned up whenever the bean it was injected into goes out of scope, the bean that produced it will remain in the session until it expires.

98. Taking a brief look at AccountService, we can see the aggregation of business method calls as AccountService utilizes UserService to create a user for an account:

99.@RequestScoped

100. public class AccountService extends AbstractService<Account> {

101.

102. @Inject

103. UserService userService;

104. public AccountService() {

105. super(Account.class);

106. }

107.

108. @Transactional

109.

110. @Secure(rolesAllowed = RoleType.GUEST)

111. public void register(Account account) throws ServiceException {

112. if (null == account.getUser()) {

113. throw new IllegalArgumentException("User can not be null on Account");

114. }

115. if (null != account.getId()) {

116. throw new EntityExistsException();

117. }

118. userService.createUser(account.getUser());

119. try {

120. getEntityManager().persist(account);

121. } catch (ConstraintViolationException cve) {

122. throw new ServiceException(cve);

123. }

124. }

}

Summary

Development of the services, interceptors, and events for our book store application has put into practice all that we have learned in the previous chapters. The chapter included only snippets of the most important code that is available, but there is also code for other services, and all the JPA entity classes with their relationships.

Looking at the code for the chapter, there are also several integration tests for the services that utilize Arquillian to perform in-container testing with JBoss AS7.