├── .gitignore ├── README.md ├── account-jsp ├── README.MD ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── bobocode │ │ │ ├── config │ │ │ ├── AccountWebAppInitializer.java │ │ │ ├── RootConfig.java │ │ │ └── WebConfig.java │ │ │ └── web │ │ │ └── controller │ │ │ ├── AccountController.java │ │ │ └── WelcomeController.java │ └── webapp │ │ └── WEB-INF │ │ └── views │ │ ├── accounts.jsp │ │ └── welcome.jsp │ └── test │ └── java │ └── com │ └── bobocode │ ├── AccountControllerTest.java │ ├── WebAppConfigTest.java │ ├── WelcomeControllerTest.java │ └── util │ └── WebAppInitializerWrapper.java ├── account-rest-api ├── README.MD ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── bobocode │ │ ├── config │ │ ├── AccountRestApiInitializer.java │ │ ├── RootConfig.java │ │ └── WebConfig.java │ │ ├── dao │ │ ├── AccountDao.java │ │ └── impl │ │ │ └── InMemoryAccountDao.java │ │ ├── exception │ │ └── EntityNotFountException.java │ │ └── web │ │ └── controller │ │ └── AccountRestController.java │ └── test │ └── java │ └── com │ └── bobocode │ └── AccountRestControllerTest.java ├── hello-application-context ├── README.MD ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── bobocode │ │ ├── config │ │ └── AppConfig.java │ │ ├── dao │ │ ├── AccountDao.java │ │ └── FakeAccountDao.java │ │ └── service │ │ └── AccountService.java │ └── test │ └── java │ └── com │ └── bobocode │ └── AppConfigTest.java ├── hello-spring-mvc ├── README.MD ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── bobocode │ │ ├── config │ │ ├── RootConfig.java │ │ ├── WebAppInitializer.java │ │ └── WebConfig.java │ │ └── web │ │ └── controller │ │ └── WelcomeController.java │ └── test │ └── java │ └── com │ └── bobocode │ ├── WebAppInitializerWrapper.java │ └── WelcomeWebAppTest.java ├── inter-bean-dependencies ├── README.MD ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── bobocode │ │ ├── config │ │ └── RootConfig.java │ │ ├── dao │ │ ├── AccountDao.java │ │ └── impl │ │ │ └── FakeAccountDao.java │ │ └── service │ │ └── AccountService.java │ └── test │ └── java │ └── com │ └── bobocode │ ├── RootConfigContextTest.java │ └── RootConfigHacksTest.java ├── pom.xml ├── spring-framework-exercises-model ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── bobocode │ └── model │ ├── Account.java │ ├── Gender.java │ └── jpa │ ├── Role.java │ ├── RoleType.java │ └── User.java ├── spring-framework-exercises-util ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── bobocode │ └── TestDataGenerator.java └── transactional-user-service ├── README.MD ├── pom.xml └── src ├── main └── java │ └── com │ └── bobocode │ ├── config │ ├── JpaConfig.java │ └── RootConfig.java │ ├── dao │ ├── UserDao.java │ └── impl │ │ └── JpaUserDao.java │ └── service │ └── UserService.java └── test └── java └── com └── bobocode └── TransactionalUserServiceTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea 3 | **/*.iml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring framework exercises 2 | The list of exercises dedicated to training your Spring framework related skills 3 | 4 | ### No pain, No gain :heavy_exclamation_mark: 5 | 6 | > Skill is only developed by hours and hours and hours of beating on your craft 7 | 8 | Working on real problems, you're focused on finding a solution. Learning new things, you're trying to understand how it works. 9 | It is important to have a different type of activities, which purpose is improving your skill 10 | 11 | ***An exercise** is a predefined task that you continuously implement to improve a certain skill* :muscle: 12 | ## 13 | * [Hello ApplicationContext](https://github.com/boy4uck/spring-framework-exercises/tree/master/hello-application-context#hello-applicationcontext-exercise-muscle) 14 | * [Hello Spring MVC](https://github.com/boy4uck/spring-framework-exercises/tree/master/hello-spring-mvc#hello-spring-mvc-exercise-muscle) 15 | * [Account JSP](https://github.com/bobocode-projects/spring-framework-exercises/tree/master/account-jsp) 16 | * [Account REST API](https://github.com/bobocode-projects/spring-framework-exercises/tree/master/account-rest-api#account-rest-api-exercise-muscle) 17 | * [Transactional User Service](https://github.com/bobocode-projects/spring-framework-exercises/tree/master/transactional-user-service#transactional-userservice-exercise-muscle) 18 | -------------------------------------------------------------------------------- /account-jsp/README.MD: -------------------------------------------------------------------------------- 1 | # Accounts JSP exercise :muscle: 2 | Improve your *Spring MVC* configuration and controller mapping skills 3 | ### Task 4 | This is a typical *Spring MVC* application with *JSP pages*. Your job is to **configure dispatcher servlet initializer** 5 | separating root and web contexts, **configure `ViewResolver`** for *JSP* views, and **implement two controllers 6 | `WelcomeController` and `AccountController`** 7 | 8 | To verify your configuration, run all tests from test package of this module 9 | 10 | 11 | ### Pre-conditions :heavy_exclamation_mark: 12 | You're supposed to be familiar with *Spring MVC* 13 | 14 | ### How to start :question: 15 | * Just clone the repository and start implementing the **todo** section, verify your changes by running tests 16 | * If you don't have enough knowledge about this domain, check out the [links below](#related-materials-information_source) 17 | * Don't worry if you got stuck, checkout the **exercise/completed** branch and see the final implementation 18 | 19 | ### Related materials :information_source: 20 | * [Spring MVC basics tutorial](https://github.com/boy4uck/spring-framework-tutorial/tree/master/spring-framework-mvc-basics) 21 | 22 | -------------------------------------------------------------------------------- /account-jsp/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-framework-exercises 7 | com.bobocode 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | account-jsp 13 | war 14 | 15 | 16 | 17 | org.springframework 18 | spring-webmvc 19 | 5.0.7.RELEASE 20 | 21 | 22 | javax.servlet 23 | javax.servlet-api 24 | 4.0.1 25 | provided 26 | 27 | 28 | com.bobocode 29 | spring-framework-exercises-util 30 | 1.0-SNAPSHOT 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | org.apache.maven.plugins 39 | maven-war-plugin 40 | 2.6 41 | 42 | false 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /account-jsp/src/main/java/com/bobocode/config/AccountWebAppInitializer.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.config; 2 | 3 | import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; 4 | 5 | /** 6 | * This class is used to configure DispatcherServlet and links it with application config classes 7 | *

8 | * todo: provide default servlet mapping ("/") 9 | * todo: use {@link WebConfig} as ServletConfig class 10 | * todo: use {@link RootConfig} as root application config class 11 | */ 12 | public class AccountWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { 13 | @Override 14 | protected Class[] getRootConfigClasses() { 15 | throw new UnsupportedOperationException("It's your job to implement this method!"); 16 | } 17 | 18 | @Override 19 | protected Class[] getServletConfigClasses() { 20 | throw new UnsupportedOperationException("It's your job to implement this method!"); 21 | } 22 | 23 | @Override 24 | protected String[] getServletMappings() { 25 | throw new UnsupportedOperationException("It's your job to implement this method!"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /account-jsp/src/main/java/com/bobocode/config/RootConfig.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.config; 2 | 3 | import com.bobocode.TestDataGenerator; 4 | import org.springframework.stereotype.Controller; 5 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 6 | 7 | /** 8 | * This class provides application root (non-web) configuration. 9 | *

10 | * todo: 1. Mark this class as config 11 | * todo: 2. Enable component scanning for all packages in "com.bobocode" using annotation property "basePackages" 12 | * todo: 3. Exclude web related config and beans (ignore @{@link Controller}, ignore {@link EnableWebMvc}) 13 | * todo: 4. Configure {@link TestDataGenerator} bean with name "dataGenerator" (don't specify name explicitly) 14 | */ 15 | public class RootConfig { 16 | } 17 | -------------------------------------------------------------------------------- /account-jsp/src/main/java/com/bobocode/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.config; 2 | 3 | /** 4 | * This class provides web (servlet) related configuration. 5 | *

6 | * todo: 1. Mark this class as Spring config class 7 | * todo: 2. Enable web mvc using annotation 8 | * todo: 3. Enable component scanning for package "web" using annotation value 9 | * todo: 4. Configure JPS internal view resolver with prefix = "/WEB-INF/views/" and suffix ".jsp" 10 | */ 11 | public class WebConfig { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /account-jsp/src/main/java/com/bobocode/web/controller/AccountController.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.web.controller; 2 | 3 | import com.bobocode.TestDataGenerator; 4 | import com.bobocode.model.Account; 5 | 6 | /** 7 | * This controller provides endpoint that generates a list of {@link Account} and passes it to the view. 8 | *

9 | * todo: 1. Configure controller that handles requests with url "/accounts" 10 | * todo: 2. Inject a {@link TestDataGenerator} 11 | * todo: 3. Implement a method that handles GET request with param "size" to url "/accounts" and forwards it to the accounts.jsp view 12 | * todo: 4. In this method generate a list of account using data generator and received int value "size" 13 | * todo: 5. Provide a default value "10" for parameter "size" 14 | * todo: 6. Pass the list of accounts to the view using model attribute with name "accountList" 15 | */ 16 | public class AccountController { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /account-jsp/src/main/java/com/bobocode/web/controller/WelcomeController.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.web.controller; 2 | 3 | /** 4 | * Welcome controller that consists of one method that handles get request to "/" and "/welcome" and respond with a message. 5 | *

6 | * todo: 1. Mark this class as Spring controller 7 | * todo: 2. Configure HTTP GET mapping "/" and "/welcome" for method {@link WelcomeController#welcome()} 8 | * todo: 3. Forward the request to "welcome.jsp" view 9 | */ 10 | public class WelcomeController { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /account-jsp/src/main/webapp/WEB-INF/views/accounts.jsp: -------------------------------------------------------------------------------- 1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> 2 | 3 | 4 | 5 | Accounts 6 | 7 | 8 |

Got ${accountList.size()} accounts

9 | 10 | 11 | -------------------------------------------------------------------------------- /account-jsp/src/main/webapp/WEB-INF/views/welcome.jsp: -------------------------------------------------------------------------------- 1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> 2 | 3 | 4 | 5 | Welcome 6 | 7 | 8 |

Welcome to Account JSP exercise

9 | 10 | 11 | -------------------------------------------------------------------------------- /account-jsp/src/test/java/com/bobocode/AccountControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.bobocode; 2 | 3 | import com.bobocode.config.RootConfig; 4 | import com.bobocode.config.WebConfig; 5 | import com.bobocode.web.controller.AccountController; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; 11 | import org.springframework.test.web.servlet.MockMvc; 12 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.context.WebApplicationContext; 15 | 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.hamcrest.Matchers.arrayContaining; 18 | import static org.hamcrest.Matchers.arrayWithSize; 19 | import static org.hamcrest.Matchers.hasSize; 20 | import static org.hamcrest.core.IsNull.notNullValue; 21 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; 23 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 24 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; 25 | 26 | @SpringJUnitWebConfig(classes = {RootConfig.class, WebConfig.class}) 27 | class AccountControllerTest { 28 | @Autowired 29 | private WebApplicationContext applicationContext; 30 | 31 | private MockMvc mockMvc; 32 | 33 | @BeforeEach 34 | void setup() { 35 | mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext).build(); 36 | } 37 | 38 | @Test 39 | void testAccountControllerAnnotation() { 40 | Controller controller = AccountController.class.getAnnotation(Controller.class); 41 | 42 | assertThat(controller, notNullValue()); 43 | } 44 | 45 | @Test 46 | void testAccountControllerRequestMapping() { 47 | RequestMapping requestMapping = AccountController.class.getAnnotation(RequestMapping.class); 48 | 49 | assertThat(requestMapping, notNullValue()); 50 | assertThat(requestMapping.value(), arrayWithSize(1)); 51 | assertThat(requestMapping.value(), arrayContaining("/accounts")); 52 | } 53 | 54 | @Test 55 | void testGetAccountsResponseStatusCode() throws Exception { 56 | mockMvc.perform(get("/accounts")).andExpect(status().isOk()); 57 | } 58 | 59 | @Test 60 | void testGetAccountsViewName() throws Exception { 61 | mockMvc.perform(get("/accounts")) 62 | .andExpect(view().name("accounts")); 63 | } 64 | 65 | @Test 66 | void testGetAccountsModelContainsAccountList() throws Exception { 67 | mockMvc.perform(get("/accounts")) 68 | .andExpect(model().attributeExists("accountList")); 69 | } 70 | 71 | @Test 72 | void testAccountsResponseDefaultListSize() throws Exception { 73 | mockMvc.perform(get("/accounts")) 74 | .andExpect(model().attribute("accountList", hasSize(10))); 75 | } 76 | 77 | @Test 78 | void testAccountsResponseListSize() throws Exception { 79 | mockMvc.perform(get("/accounts").param("size", "20")) 80 | .andExpect(model().attribute("accountList", hasSize(20))); 81 | } 82 | 83 | 84 | } 85 | -------------------------------------------------------------------------------- /account-jsp/src/test/java/com/bobocode/WebAppConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.bobocode; 2 | 3 | import com.bobocode.config.RootConfig; 4 | import com.bobocode.config.WebConfig; 5 | import com.bobocode.util.WebAppInitializerWrapper; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.ApplicationContext; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.ComponentScan; 11 | import org.springframework.context.annotation.ComponentScan.Filter; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.context.annotation.FilterType; 14 | import org.springframework.stereotype.Controller; 15 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 16 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 17 | 18 | import java.lang.reflect.Method; 19 | import java.util.List; 20 | import java.util.stream.Collectors; 21 | import java.util.stream.Stream; 22 | 23 | import static org.hamcrest.MatcherAssert.assertThat; 24 | import static org.hamcrest.Matchers.arrayContaining; 25 | import static org.hamcrest.Matchers.arrayWithSize; 26 | import static org.hamcrest.Matchers.containsInAnyOrder; 27 | import static org.hamcrest.Matchers.equalTo; 28 | import static org.hamcrest.Matchers.notNullValue; 29 | 30 | @SpringJUnitConfig(classes = RootConfig.class) 31 | class WebAppConfigTest { 32 | 33 | @Autowired 34 | private ApplicationContext applicationContext; 35 | 36 | @Test 37 | void testDispatcherServletMapping() { 38 | WebAppInitializerWrapper webAppInitializerWrapper = new WebAppInitializerWrapper(); 39 | 40 | assertThat(webAppInitializerWrapper.getServletMappings(), arrayContaining("/")); 41 | } 42 | 43 | @Test 44 | void testInitializerRootConfigClasses() { 45 | WebAppInitializerWrapper webAppInitializerWrapper = new WebAppInitializerWrapper(); 46 | 47 | assertThat(webAppInitializerWrapper.getRootConfigClasses(), arrayContaining(RootConfig.class)); 48 | } 49 | 50 | @Test 51 | void testInitializerWebConfigClasses() { 52 | WebAppInitializerWrapper webAppInitializerWrapper = new WebAppInitializerWrapper(); 53 | 54 | assertThat(webAppInitializerWrapper.getServletConfigClasses(), arrayContaining(WebConfig.class)); 55 | } 56 | 57 | @Test 58 | void testRootConfigClassIsMarkedAsConfiguration() { 59 | Configuration configuration = RootConfig.class.getAnnotation(Configuration.class); 60 | 61 | assertThat(configuration, notNullValue()); 62 | } 63 | 64 | @Test 65 | void testRootConfigClassEnablesComponentScan() { 66 | ComponentScan componentScan = RootConfig.class.getAnnotation(ComponentScan.class); 67 | 68 | assertThat(componentScan, notNullValue()); 69 | } 70 | 71 | @Test 72 | void testRootConfigComponentScanPackages() { 73 | ComponentScan componentScan = RootConfig.class.getAnnotation(ComponentScan.class); 74 | 75 | assertThat(componentScan.basePackages(), arrayContaining("com.bobocode")); 76 | } 77 | 78 | @Test 79 | void testRootConfigComponentScanFilters() { 80 | ComponentScan componentScan = RootConfig.class.getAnnotation(ComponentScan.class); 81 | Filter[] filters = componentScan.excludeFilters(); 82 | List filteredClasses = getFilteredClasses(filters); 83 | 84 | assertThat(filters, arrayWithSize(2)); 85 | assertThat(filters[0].type(), equalTo(FilterType.ANNOTATION)); 86 | assertThat(filters[1].type(), equalTo(FilterType.ANNOTATION)); 87 | assertThat(filteredClasses, containsInAnyOrder(EnableWebMvc.class, Controller.class)); 88 | } 89 | 90 | private List getFilteredClasses(Filter[] filters) { 91 | return Stream.of(filters).flatMap(filter -> Stream.of(filter.value())).collect(Collectors.toList()); 92 | } 93 | 94 | @Test 95 | void testWebConfigIsMarkedAsConfiguration() { 96 | Configuration configuration = WebConfig.class.getAnnotation(Configuration.class); 97 | 98 | assertThat(configuration, notNullValue()); 99 | } 100 | 101 | @Test 102 | void testWebConfigEnablesComponentScan() { 103 | ComponentScan componentScan = WebConfig.class.getAnnotation(ComponentScan.class); 104 | 105 | assertThat(componentScan, notNullValue()); 106 | } 107 | 108 | @Test 109 | void testWebConfigComponentScanPackages() { 110 | ComponentScan componentScan = WebConfig.class.getAnnotation(ComponentScan.class); 111 | 112 | assertThat(componentScan.value(), arrayContaining("com.bobocode.web")); 113 | } 114 | 115 | @Test 116 | void testWebConfigEnablesWebMvc() { 117 | EnableWebMvc enableWebMvc = WebConfig.class.getAnnotation(EnableWebMvc.class); 118 | 119 | assertThat(enableWebMvc, notNullValue()); 120 | } 121 | 122 | @Test 123 | void testDataGeneratorBeanName() { 124 | TestDataGenerator dataGenerator = applicationContext.getBean("dataGenerator", TestDataGenerator.class); 125 | 126 | assertThat(dataGenerator, notNullValue()); 127 | } 128 | 129 | @Test 130 | void testDataGeneratorBeanNameIsNotSpecifiedExplicitly() { 131 | Method[] methods = RootConfig.class.getMethods(); 132 | Method testDataGeneratorBeanMethod = findTestDataGeneratorBeanMethod(methods); 133 | Bean bean = testDataGeneratorBeanMethod.getDeclaredAnnotation(Bean.class); 134 | 135 | assertThat(bean.name(), arrayWithSize(0)); 136 | assertThat(bean.value(), arrayWithSize(0)); 137 | } 138 | 139 | private Method findTestDataGeneratorBeanMethod(Method[] methods) { 140 | for (Method method : methods) { 141 | if (method.getReturnType().equals(TestDataGenerator.class) 142 | && method.getDeclaredAnnotation(Bean.class) != null) { 143 | return method; 144 | } 145 | } 146 | return null; 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /account-jsp/src/test/java/com/bobocode/WelcomeControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.bobocode; 2 | 3 | import com.bobocode.config.RootConfig; 4 | import com.bobocode.config.WebConfig; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; 9 | import org.springframework.test.web.servlet.MockMvc; 10 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 11 | import org.springframework.web.context.WebApplicationContext; 12 | 13 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; 16 | 17 | @SpringJUnitWebConfig(classes = {RootConfig.class, WebConfig.class}) 18 | class WelcomeControllerTest { 19 | @Autowired 20 | private WebApplicationContext applicationContext; 21 | 22 | private MockMvc mockMvc; 23 | 24 | @BeforeEach 25 | void setup() { 26 | mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext).build(); 27 | } 28 | 29 | @Test 30 | void testWelcomeUrl() throws Exception { 31 | mockMvc.perform(get("/welcome")) 32 | .andExpect(status().isOk()); 33 | } 34 | 35 | @Test 36 | void testRootUrl() throws Exception { 37 | mockMvc.perform(get("/")) 38 | .andExpect(status().isOk()); 39 | } 40 | 41 | @Test 42 | void testWelcomeViewName() throws Exception { 43 | mockMvc.perform(get("/")) 44 | .andExpect(view().name("welcome")); 45 | } 46 | 47 | 48 | } 49 | -------------------------------------------------------------------------------- /account-jsp/src/test/java/com/bobocode/util/WebAppInitializerWrapper.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.util; 2 | 3 | import com.bobocode.config.AccountWebAppInitializer; 4 | 5 | public class WebAppInitializerWrapper extends AccountWebAppInitializer { 6 | public String[] getServletMappings() { 7 | return super.getServletMappings(); 8 | } 9 | 10 | public Class[] getRootConfigClasses() { 11 | return super.getRootConfigClasses(); 12 | } 13 | 14 | public Class[] getServletConfigClasses() { 15 | return super.getServletConfigClasses(); 16 | } 17 | } -------------------------------------------------------------------------------- /account-rest-api/README.MD: -------------------------------------------------------------------------------- 1 | # Account REST API exercise :muscle: 2 | Improve your *Spring MVC* configuration and rest mapping skills 3 | ### Task 4 | This webapp provides a **simple REST API for `Account`**. The data is stored using in-memory fake DAO. Your job is to 5 | **configure Spring MVC application** and **implement AccountRestController**. In order to complete the task, please 6 | **follow the instructions in the *todo* section** 7 | 8 | To verify your configuration, run `AccountRestControllerTest.java` :white_check_mark: 9 | 10 | 11 | ### Pre-conditions :heavy_exclamation_mark: 12 | You're supposed to be familiar with *Spring MVC* 13 | 14 | ### How to start :question: 15 | * Just clone the repository and start implementing the **todo** section, verify your changes by running tests 16 | * If you don't have enough knowledge about this domain, check out the [links below](#related-materials-information_source) 17 | * Don't worry if you got stuck, checkout the **exercise/completed** branch and see the final implementation 18 | 19 | ### Related materials :information_source: 20 | * [Spring REST basics tutorial](https://github.com/bobocode-projects/spring-framework-tutorial/tree/master/rest-basics) 21 | 22 | -------------------------------------------------------------------------------- /account-rest-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-framework-exercises 7 | com.bobocode 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | account-rest-api 13 | war 14 | 15 | 16 | 17 | org.springframework 18 | spring-webmvc 19 | 5.0.7.RELEASE 20 | 21 | 22 | javax.servlet 23 | javax.servlet-api 24 | 4.0.1 25 | provided 26 | 27 | 28 | com.bobocode 29 | spring-framework-exercises-util 30 | 1.0-SNAPSHOT 31 | 32 | 33 | com.fasterxml.jackson.core 34 | jackson-core 35 | 2.9.7 36 | 37 | 38 | com.fasterxml.jackson.core 39 | jackson-databind 40 | 2.9.7 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.apache.maven.plugins 49 | maven-war-plugin 50 | 2.6 51 | 52 | false 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /account-rest-api/src/main/java/com/bobocode/config/AccountRestApiInitializer.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.config; 2 | 3 | import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; 4 | 5 | public class AccountRestApiInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { 6 | @Override 7 | protected Class[] getRootConfigClasses() { 8 | return new Class[]{RootConfig.class}; 9 | } 10 | 11 | @Override 12 | protected Class[] getServletConfigClasses() { 13 | return new Class[]{WebConfig.class}; 14 | } 15 | 16 | @Override 17 | protected String[] getServletMappings() { 18 | return new String[]{"/"}; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /account-rest-api/src/main/java/com/bobocode/config/RootConfig.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.config; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 5 | 6 | /** 7 | * This class provides application root (non-web) configuration. 8 | *

9 | * todo: 1. Mark this class as config 10 | * todo: 2. Enable component scanning for all packages in "com.bobocode" using annotation property "basePackages" 11 | * todo: 3. Exclude web related config and beans (ignore @{@link Controller}, ignore {@link EnableWebMvc}) 12 | */ 13 | public class RootConfig { 14 | } 15 | -------------------------------------------------------------------------------- /account-rest-api/src/main/java/com/bobocode/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.config; 2 | 3 | /** 4 | * This class provides web (servlet) related configuration. 5 | *

6 | * todo: 1. Mark this class as Spring config class 7 | * todo: 2. Enable web mvc using annotation 8 | * todo: 3. Enable component scanning for package "web" using annotation value 9 | */ 10 | public class WebConfig { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /account-rest-api/src/main/java/com/bobocode/dao/AccountDao.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.dao; 2 | 3 | import com.bobocode.model.Account; 4 | 5 | import java.util.List; 6 | 7 | public interface AccountDao { 8 | List findAll(); 9 | 10 | Account findById(long id); 11 | 12 | Account save(Account account); 13 | 14 | void remove(Account account); 15 | } 16 | -------------------------------------------------------------------------------- /account-rest-api/src/main/java/com/bobocode/dao/impl/InMemoryAccountDao.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.dao.impl; 2 | 3 | import com.bobocode.dao.AccountDao; 4 | import com.bobocode.exception.EntityNotFountException; 5 | import com.bobocode.model.Account; 6 | 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | /** 13 | * {@link AccountDao} implementation that is based on {@link java.util.HashMap}. 14 | *

15 | * todo: 1. Configure a component with name "accountDao" 16 | */ 17 | public class InMemoryAccountDao implements AccountDao { 18 | private Map accountMap = new HashMap<>(); 19 | private long idSequence = 1L; 20 | 21 | @Override 22 | public List findAll() { 23 | return new ArrayList<>(accountMap.values()); 24 | } 25 | 26 | @Override 27 | public Account findById(long id) { 28 | Account account = accountMap.get(id); 29 | if (account == null) { 30 | throw new EntityNotFountException(String.format("Cannot found account by id = %d", id)); 31 | } 32 | return account; 33 | } 34 | 35 | @Override 36 | public Account save(Account account) { 37 | if (account.getId() == null) { 38 | account.setId(idSequence++); 39 | } 40 | accountMap.put(account.getId(), account); 41 | return account; 42 | } 43 | 44 | @Override 45 | public void remove(Account account) { 46 | accountMap.remove(account.getId()); 47 | } 48 | 49 | public void clear() { 50 | accountMap.clear(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /account-rest-api/src/main/java/com/bobocode/exception/EntityNotFountException.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.exception; 2 | 3 | public class EntityNotFountException extends RuntimeException { 4 | public EntityNotFountException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /account-rest-api/src/main/java/com/bobocode/web/controller/AccountRestController.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.web.controller; 2 | 3 | import com.bobocode.dao.AccountDao; 4 | 5 | /** 6 | *

7 | * todo: 1. Configure rest controller that handles requests with url "/accounts" 8 | * todo: 2. Inject {@link AccountDao} implementation 9 | * todo: 3. Implement method that handles GET request and returns a list of accounts 10 | * todo: 4. Implement method that handles GET request with id as path variable and returns account by id 11 | * todo: 5. Implement method that handles POST request, receives account as request body, saves account and returns it 12 | * todo: Configure HTTP response status code 201 - CREATED 13 | * todo: 6. Implement method that handles PUT request with id as path variable and receives account as request body. 14 | * todo: It check if account id and path variable are the same and throws {@link IllegalStateException} otherwise. 15 | * todo: Then it saves received account. Configure HTTP response status code 204 - NO CONTENT 16 | * todo: 7. Implement method that handles DELETE request with id as path variable removes an account by id 17 | * todo: Configure HTTP response status code 204 - NO CONTENT 18 | */ 19 | public class AccountRestController { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /account-rest-api/src/test/java/com/bobocode/AccountRestControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.bobocode; 2 | 3 | import com.bobocode.config.RootConfig; 4 | import com.bobocode.config.WebConfig; 5 | import com.bobocode.dao.impl.InMemoryAccountDao; 6 | import com.bobocode.model.Account; 7 | import com.bobocode.web.controller.AccountRestController; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; 13 | import org.springframework.test.web.servlet.MockMvc; 14 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RestController; 17 | import org.springframework.web.context.WebApplicationContext; 18 | 19 | import static org.hamcrest.MatcherAssert.assertThat; 20 | import static org.hamcrest.Matchers.arrayContaining; 21 | import static org.hamcrest.Matchers.arrayWithSize; 22 | import static org.hamcrest.Matchers.hasItems; 23 | import static org.hamcrest.core.IsNull.notNullValue; 24 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 25 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 26 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 27 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; 28 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 29 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 30 | 31 | @SpringJUnitWebConfig(classes = {RootConfig.class, WebConfig.class}) 32 | class AccountRestControllerTest { 33 | @Autowired 34 | private WebApplicationContext applicationContext; 35 | 36 | @Autowired 37 | private InMemoryAccountDao accountDao; 38 | 39 | private MockMvc mockMvc; 40 | 41 | @BeforeEach 42 | void setup() { 43 | mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext).build(); 44 | accountDao.clear(); 45 | } 46 | 47 | @Test 48 | void testAccountRestControllerAnnotation() { 49 | RestController restController = AccountRestController.class.getAnnotation(RestController.class); 50 | 51 | assertThat(restController, notNullValue()); 52 | } 53 | 54 | @Test 55 | void testAccountRestControllerRequestMapping() { 56 | RequestMapping requestMapping = AccountRestController.class.getAnnotation(RequestMapping.class); 57 | 58 | assertThat(requestMapping, notNullValue()); 59 | assertThat(requestMapping.value(), arrayWithSize(1)); 60 | assertThat(requestMapping.value(), arrayContaining("/accounts")); 61 | } 62 | 63 | @Test 64 | void testHttpStatusCodeOnCreate() throws Exception { 65 | mockMvc.perform( 66 | post("/accounts") 67 | .contentType(MediaType.APPLICATION_JSON) 68 | .content("{\"firstName\":\"Johnny\", \"lastName\":\"Boy\", \"email\":\"jboy@gmail.com\"}")) 69 | .andExpect(status().isCreated()); 70 | } 71 | 72 | @Test 73 | void testCreateAccountReturnsAssignedId() throws Exception { 74 | mockMvc.perform( 75 | post("/accounts") 76 | .contentType(MediaType.APPLICATION_JSON) 77 | .content("{\"firstName\":\"Johnny\", \"lastName\":\"Boy\", \"email\":\"jboy@gmail.com\"}")) 78 | .andExpect(jsonPath("$.id").value(1L)); 79 | } 80 | 81 | @Test 82 | void testGetAccountsResponseStatusCode() throws Exception { 83 | mockMvc.perform(get("/accounts").accept(MediaType.APPLICATION_JSON_UTF8)) 84 | .andExpect(status().isOk()); 85 | } 86 | 87 | @Test 88 | void testGetAllAccounts() throws Exception { 89 | Account account1 = create("Johnny", "Boy", "jboy@gmail.com"); 90 | Account account2 = create("Okko", "Bay", "obay@gmail.com"); 91 | accountDao.save(account1); 92 | accountDao.save(account2); 93 | 94 | mockMvc.perform(get("/accounts")) 95 | .andExpect(status().isOk()) 96 | .andExpect(jsonPath("$.[*].email").value(hasItems("jboy@gmail.com", "obay@gmail.com"))); 97 | } 98 | 99 | private Account create(String firstName, String lastName, String email) { 100 | Account account = new Account(); 101 | account.setFirstName(firstName); 102 | account.setLastName(lastName); 103 | account.setEmail(email); 104 | return account; 105 | } 106 | 107 | @Test 108 | void testGetById() throws Exception { 109 | Account account = create("Johnny", "Boy", "jboy@gmail.com"); 110 | accountDao.save(account); 111 | 112 | mockMvc.perform(get(String.format("/accounts/%d", account.getId()))) 113 | .andExpect(status().isOk()) 114 | .andExpect(jsonPath("$.id").value(account.getId())) 115 | .andExpect(jsonPath("$.email").value("jboy@gmail.com")) 116 | .andExpect(jsonPath("$.firstName").value("Johnny")) 117 | .andExpect(jsonPath("$.lastName").value("Boy")); 118 | } 119 | 120 | @Test 121 | void testRemoveAccount() throws Exception { 122 | Account account = create("Johnny", "Boy", "jboy@gmail.com"); 123 | accountDao.save(account); 124 | 125 | mockMvc.perform(delete(String.format("/accounts/%d", account.getId()))) 126 | .andExpect(status().isNoContent()); 127 | } 128 | 129 | @Test 130 | void testUpdateAccount() throws Exception { 131 | Account account = create("Johnny", "Boy", "jboy@gmail.com"); 132 | accountDao.save(account); 133 | 134 | mockMvc.perform(put(String.format("/accounts/%d", account.getId())).contentType(MediaType.APPLICATION_JSON) 135 | .content(String.format("{\"id\":\"%d\", \"firstName\":\"Johnny\", \"lastName\":\"Boy\", \"email\":\"johnny.boy@gmail.com\"}", account.getId()))) 136 | .andExpect(status().isNoContent()); 137 | } 138 | 139 | 140 | } 141 | -------------------------------------------------------------------------------- /hello-application-context/README.MD: -------------------------------------------------------------------------------- 1 | # Hello ApplicationContext exercise :muscle: 2 | Improve your *Spring ApplicationContext* Java configuration skills 3 | ### Task 4 | The task is to **configure `ApplicationContext`** that contains `AccountService` bean, `AccountDao` bean 5 | and `TestDataGenerator` bean. Your job is to follow the instructions in the *todo* section and **implement 6 | a proper configuration.** 7 | 8 | To verify your configuration, run `AppConfigTest.java` 9 | 10 | 11 | ### Pre-conditions :heavy_exclamation_mark: 12 | You're supposed to be familiar with *Spring IoC* and *Dependency injection* 13 | 14 | ### How to start :question: 15 | * Just clone the repository and start implementing the **todo** section, verify your changes by running tests 16 | * If you don't have enough knowledge about this domain, check out the [links below](#related-materials-information_source) 17 | * Don't worry if you got stuck, checkout the **exercise/completed** branch and see the final implementation 18 | 19 | ### Related materials :information_source: 20 | * [Spring IoC basics tutorial](https://github.com/boy4uck/spring-framework-tutorial/tree/master/spring-framework-ioc-basics) 21 | 22 | -------------------------------------------------------------------------------- /hello-application-context/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-framework-exercises 7 | com.bobocode 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | hello-application-context 13 | 14 | 15 | 16 | com.bobocode 17 | spring-framework-exercises-util 18 | 1.0-SNAPSHOT 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /hello-application-context/src/main/java/com/bobocode/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.config; 2 | 3 | import com.bobocode.TestDataGenerator; 4 | 5 | /** 6 | * This class application context configuration. 7 | *

8 | * todo: make this class a Spring configuration class 9 | * todo: enable component scanning for dao and service packages 10 | * todo: provide explicit configuration for a bean of type {@link TestDataGenerator} with name "dataGenerator" in this class. 11 | * todo: Don't specify bean name "dataGenerator" explicitly 12 | */ 13 | public class AppConfig { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /hello-application-context/src/main/java/com/bobocode/dao/AccountDao.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.dao; 2 | 3 | import com.bobocode.model.Account; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Defines an API for {@link Account} data access object (DAO). 9 | */ 10 | public interface AccountDao { 11 | List findAll(); 12 | } 13 | -------------------------------------------------------------------------------- /hello-application-context/src/main/java/com/bobocode/dao/FakeAccountDao.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.dao; 2 | 3 | import com.bobocode.TestDataGenerator; 4 | import com.bobocode.model.Account; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | 7 | import java.util.List; 8 | import java.util.stream.Stream; 9 | 10 | import static java.util.stream.Collectors.toList; 11 | 12 | /** 13 | * Provides a fake {@link AccountDao} implementation that uses generated fake data. 14 | *

15 | * todo: configure this class as Spring component with bean name "accountDao" 16 | * todo: use explicit (with {@link Autowired} annotation) constructor-based dependency injection 17 | */ 18 | public class FakeAccountDao implements AccountDao { 19 | private List accounts; 20 | 21 | public FakeAccountDao(TestDataGenerator testDataGenerator) { 22 | this.accounts = Stream.generate(testDataGenerator::generateAccount) 23 | .limit(20) 24 | .collect(toList()); 25 | } 26 | 27 | @Override 28 | public List findAll() { 29 | return accounts; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /hello-application-context/src/main/java/com/bobocode/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.service; 2 | 3 | import com.bobocode.dao.AccountDao; 4 | import com.bobocode.model.Account; 5 | 6 | import java.util.Comparator; 7 | import java.util.List; 8 | 9 | /** 10 | * Provides service API for {@link Account}. 11 | *

12 | * todo: configure {@link AccountService} bean implicitly using special annotation for service classes 13 | * todo: use implicit constructor-based dependency injection (don't use {@link org.springframework.beans.factory.annotation.Autowired}) 14 | */ 15 | public class AccountService { 16 | private final AccountDao accountDao; 17 | 18 | public AccountService(AccountDao accountDao) { 19 | this.accountDao = accountDao; 20 | } 21 | 22 | public Account findRichestAccount() { 23 | List accounts = accountDao.findAll(); 24 | return accounts.stream() 25 | .max(Comparator.comparing(Account::getBalance)) 26 | .get(); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /hello-application-context/src/test/java/com/bobocode/AppConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.bobocode; 2 | 3 | import com.bobocode.config.AppConfig; 4 | import com.bobocode.dao.AccountDao; 5 | import com.bobocode.dao.FakeAccountDao; 6 | import com.bobocode.model.Account; 7 | import com.bobocode.service.AccountService; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.context.ApplicationContext; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.ComponentScan; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.stereotype.Component; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 17 | 18 | import java.lang.annotation.Annotation; 19 | import java.lang.reflect.Method; 20 | import java.util.Comparator; 21 | import java.util.Map; 22 | 23 | import static org.hamcrest.MatcherAssert.assertThat; 24 | import static org.hamcrest.Matchers.arrayContainingInAnyOrder; 25 | import static org.hamcrest.Matchers.arrayWithSize; 26 | import static org.hamcrest.Matchers.equalTo; 27 | import static org.hamcrest.Matchers.hasItem; 28 | import static org.hamcrest.Matchers.is; 29 | import static org.hamcrest.Matchers.notNullValue; 30 | 31 | 32 | @SpringJUnitConfig 33 | class AppConfigTest { 34 | @Configuration 35 | @ComponentScan(basePackages = "com.bobocode") 36 | static class TestConfig { 37 | } 38 | 39 | @Autowired 40 | private ApplicationContext applicationContext; 41 | 42 | @Autowired 43 | private AccountService accountService; 44 | 45 | @Autowired 46 | private AccountDao accountDao; 47 | 48 | @Test 49 | void testConfigClassIsMarkedAsConfiguration() { 50 | Configuration configuration = AppConfig.class.getAnnotation(Configuration.class); 51 | 52 | assertThat(configuration, notNullValue()); 53 | } 54 | 55 | @Test 56 | void testComponentScanIsEnabled() { 57 | ComponentScan componentScan = AppConfig.class.getAnnotation(ComponentScan.class); 58 | 59 | assertThat(componentScan, notNullValue()); 60 | } 61 | 62 | @Test 63 | void testComponentScanPackagesAreSpecified() { 64 | ComponentScan componentScan = AppConfig.class.getAnnotation(ComponentScan.class); 65 | String[] packages = componentScan.basePackages(); 66 | if (packages.length == 0) { 67 | packages = componentScan.value(); 68 | } 69 | assertThat(packages, arrayContainingInAnyOrder("com.bobocode.dao", "com.bobocode.service")); 70 | } 71 | 72 | @Test 73 | void testDataGeneratorHasOnlyOneBean() { 74 | Map testDataGeneratorMap = applicationContext.getBeansOfType(TestDataGenerator.class); 75 | 76 | assertThat(testDataGeneratorMap.size(), is(1)); 77 | } 78 | 79 | @Test 80 | void testDataGeneratorBeanIsConfiguredExplicitly() { 81 | Method[] methods = AppConfig.class.getMethods(); 82 | Method testDataGeneratorBeanMethod = findTestDataGeneratorBeanMethod(methods); 83 | 84 | 85 | assertThat(testDataGeneratorBeanMethod, notNullValue()); 86 | } 87 | 88 | @Test 89 | void testDataGeneratorBeanName() { 90 | Map dataGeneratorBeanMap = applicationContext.getBeansOfType(TestDataGenerator.class); 91 | 92 | assertThat(dataGeneratorBeanMap.keySet(), hasItem("dataGenerator")); 93 | } 94 | 95 | @Test 96 | void testDataGeneratorBeanNameIsNotSpecifiedExplicitly() { 97 | Method[] methods = AppConfig.class.getMethods(); 98 | Method testDataGeneratorBeanMethod = findTestDataGeneratorBeanMethod(methods); 99 | Bean bean = testDataGeneratorBeanMethod.getDeclaredAnnotation(Bean.class); 100 | 101 | assertThat(bean.name(), arrayWithSize(0)); 102 | assertThat(bean.value(), arrayWithSize(0)); 103 | } 104 | 105 | private Method findTestDataGeneratorBeanMethod(Method[] methods) { 106 | for (Method method : methods) { 107 | if (method.getReturnType().equals(TestDataGenerator.class) 108 | && method.getDeclaredAnnotation(Bean.class) != null) { 109 | return method; 110 | } 111 | } 112 | return null; 113 | } 114 | 115 | @Test 116 | void testFakeAccountDaoIsConfiguredAsComponent() { 117 | Component component = FakeAccountDao.class.getAnnotation(Component.class); 118 | 119 | assertThat(component, notNullValue()); 120 | } 121 | 122 | @Test 123 | void testAccountDaoHasOnlyOneBean() { 124 | Map accountDaoBeanMap = applicationContext.getBeansOfType(AccountDao.class); 125 | 126 | assertThat(accountDaoBeanMap.size(), is(1)); 127 | } 128 | 129 | @Test 130 | void testAccountDaoBeanName() { 131 | Map accountDaoBeanMap = applicationContext.getBeansOfType(AccountDao.class); 132 | 133 | assertThat(accountDaoBeanMap.keySet(), hasItem("accountDao")); 134 | } 135 | 136 | @Test 137 | void testAccountDaoConstructorIsMarkedWithAutowired() throws NoSuchMethodException { 138 | Autowired autowired = FakeAccountDao.class.getConstructor(TestDataGenerator.class).getAnnotation(Autowired.class); 139 | 140 | assertThat(autowired, notNullValue()); 141 | } 142 | 143 | @Test 144 | void testAccountServiceHasOnlyOneBean() { 145 | Map accountServiceMap = applicationContext.getBeansOfType(AccountService.class); 146 | 147 | assertThat(accountServiceMap.size(), is(1)); 148 | } 149 | 150 | @Test 151 | void testAccountServiceIsConfiguredAsService() { 152 | Service service = AccountService.class.getAnnotation(Service.class); 153 | 154 | assertThat(service, notNullValue()); 155 | } 156 | 157 | @Test 158 | void testAccountServiceBeanName() { 159 | Map accountServiceMap = applicationContext.getBeansOfType(AccountService.class); 160 | 161 | assertThat(accountServiceMap.keySet(), hasItem("accountService")); 162 | } 163 | 164 | @Test 165 | void testAccountServiceBeanNameIsNotSpecifiedExplicitly() { 166 | Service service = AccountService.class.getAnnotation(Service.class); 167 | 168 | assertThat(service.value(), equalTo("")); 169 | } 170 | 171 | @Test 172 | void testAccountServiceDoesNotUseAutowired() throws NoSuchMethodException { 173 | Annotation[] annotations = AccountService.class.getConstructor(AccountDao.class).getDeclaredAnnotations(); 174 | 175 | assertThat(annotations, arrayWithSize(0)); 176 | } 177 | 178 | @Test 179 | void testFindRichestAccount() { 180 | Account richestAccount = accountService.findRichestAccount(); 181 | 182 | Account actualRichestAccount = accountDao.findAll().stream().max(Comparator.comparing(Account::getBalance)).get(); 183 | 184 | assertThat(richestAccount, equalTo(actualRichestAccount)); 185 | } 186 | 187 | 188 | } 189 | -------------------------------------------------------------------------------- /hello-spring-mvc/README.MD: -------------------------------------------------------------------------------- 1 | # Hello Spring MVC exercise :muscle: 2 | Improve your *Spring MVC* configuration skills 3 | ### Task 4 | The task is to **configure a typical *Spring MVC* application** separating web and root configs. Your job is to follow 5 | the instructions in the *todo* section and **implement a proper configuration.** 6 | 7 | To verify your configuration, run `WelcomeWebAppTest.java` 8 | 9 | 10 | ### Pre-conditions :heavy_exclamation_mark: 11 | You're supposed to be familiar with *Spring MVC* 12 | 13 | ### How to start :question: 14 | * Just clone the repository and start implementing the **todo** section, verify your changes by running tests 15 | * If you don't have enough knowledge about this domain, check out the [links below](#related-materials-information_source) 16 | * Don't worry if you got stuck, checkout the **exercise/completed** branch and see the final implementation 17 | 18 | ### Related materials :information_source: 19 | * [Spring MVC basics tutorial](https://github.com/boy4uck/spring-framework-tutorial/tree/master/spring-framework-mvc-basics) 20 | 21 | -------------------------------------------------------------------------------- /hello-spring-mvc/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-framework-exercises 7 | com.bobocode 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | hello-spring-mvc 13 | war 14 | 15 | 16 | 17 | org.springframework 18 | spring-webmvc 19 | 5.0.7.RELEASE 20 | 21 | 22 | javax.servlet 23 | javax.servlet-api 24 | 4.0.1 25 | provided 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | org.apache.maven.plugins 34 | maven-war-plugin 35 | 2.6 36 | 37 | false 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /hello-spring-mvc/src/main/java/com/bobocode/config/RootConfig.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.config; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 5 | 6 | /** 7 | * This class provides application root (non-web) configuration. 8 | *

9 | * todo: mark this class as config 10 | * todo: enable component scanning for all packages in "com.bobocode" 11 | * todo: ignore all web related config and beans (ignore @{@link Controller}, ignore {@link EnableWebMvc}) using exclude filter 12 | */ 13 | public class RootConfig { 14 | } 15 | -------------------------------------------------------------------------------- /hello-spring-mvc/src/main/java/com/bobocode/config/WebAppInitializer.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.config; 2 | 3 | import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; 4 | 5 | /** 6 | * This class is used to configure DispatcherServlet and links it with application config classes 7 | *

8 | * todo: provide default servlet mapping ("/") 9 | * todo: use {@link WebConfig} as ServletConfig class 10 | * todo: use {@link RootConfig} as root application config class 11 | */ 12 | public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { 13 | @Override 14 | protected Class[] getRootConfigClasses() { 15 | throw new UnsupportedOperationException("Method is not implemented yet!"); 16 | } 17 | 18 | @Override 19 | protected Class[] getServletConfigClasses() { 20 | throw new UnsupportedOperationException("Method is not implemented yet!"); 21 | } 22 | 23 | @Override 24 | protected String[] getServletMappings() { 25 | throw new UnsupportedOperationException("Method is not implemented yet!"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /hello-spring-mvc/src/main/java/com/bobocode/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.config; 2 | 3 | /** 4 | * This class provides web (servlet) related configuration. 5 | *

6 | * todo: mark this class as Spring config class 7 | * todo: enable web mvc using annotation 8 | * todo: enable component scanning for package "web" 9 | */ 10 | public class WebConfig { 11 | } 12 | -------------------------------------------------------------------------------- /hello-spring-mvc/src/main/java/com/bobocode/web/controller/WelcomeController.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.web.controller; 2 | 3 | /** 4 | * Welcome controller that consists of one method that handles get request to "/welcome" and respond with a message. 5 | *

6 | * todo: mark this class as Spring controller 7 | * todo: configure HTTP GET mapping "/welcome" for method {@link WelcomeController#welcome()} 8 | * todo: tell Spring that {@link WelcomeController#welcome()} method provides response body 9 | */ 10 | public class WelcomeController { 11 | 12 | public String welcome() { 13 | return "Welcome to Spring MVC!"; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /hello-spring-mvc/src/test/java/com/bobocode/WebAppInitializerWrapper.java: -------------------------------------------------------------------------------- 1 | package com.bobocode; 2 | 3 | import com.bobocode.config.WebAppInitializer; 4 | 5 | class WebAppInitializerWrapper extends WebAppInitializer { 6 | public String[] getServletMappings() { 7 | return super.getServletMappings(); 8 | } 9 | 10 | public Class[] getRootConfigClasses() { 11 | return super.getRootConfigClasses(); 12 | } 13 | 14 | public Class[] getServletConfigClasses() { 15 | return super.getServletConfigClasses(); 16 | } 17 | } -------------------------------------------------------------------------------- /hello-spring-mvc/src/test/java/com/bobocode/WelcomeWebAppTest.java: -------------------------------------------------------------------------------- 1 | package com.bobocode; 2 | 3 | import com.bobocode.config.RootConfig; 4 | import com.bobocode.config.WebConfig; 5 | import com.bobocode.web.controller.WelcomeController; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.context.annotation.ComponentScan.Filter; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.annotation.FilterType; 11 | import org.springframework.stereotype.Controller; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 14 | 15 | import java.util.List; 16 | import java.util.stream.Collectors; 17 | import java.util.stream.Stream; 18 | 19 | import static org.hamcrest.MatcherAssert.assertThat; 20 | import static org.hamcrest.Matchers.arrayContaining; 21 | import static org.hamcrest.Matchers.arrayWithSize; 22 | import static org.hamcrest.Matchers.containsInAnyOrder; 23 | import static org.hamcrest.Matchers.equalTo; 24 | import static org.hamcrest.Matchers.notNullValue; 25 | 26 | class WelcomeWebAppTest { 27 | 28 | @Test 29 | void testDispatcherServletMapping() { 30 | WebAppInitializerWrapper webAppInitializerWrapper = new WebAppInitializerWrapper(); 31 | 32 | assertThat(webAppInitializerWrapper.getServletMappings(), arrayContaining("/")); 33 | } 34 | 35 | @Test 36 | void testInitializerRootConfigClasses() { 37 | WebAppInitializerWrapper webAppInitializerWrapper = new WebAppInitializerWrapper(); 38 | 39 | assertThat(webAppInitializerWrapper.getRootConfigClasses(), arrayContaining(RootConfig.class)); 40 | } 41 | 42 | @Test 43 | void testInitializerWebConfigClasses() { 44 | WebAppInitializerWrapper webAppInitializerWrapper = new WebAppInitializerWrapper(); 45 | 46 | assertThat(webAppInitializerWrapper.getServletConfigClasses(), arrayContaining(WebConfig.class)); 47 | } 48 | 49 | @Test 50 | void testRootConfigClassIsMarkedAsConfiguration() { 51 | Configuration configuration = RootConfig.class.getAnnotation(Configuration.class); 52 | 53 | assertThat(configuration, notNullValue()); 54 | } 55 | 56 | @Test 57 | void testRootConfigClassEnablesComponentScan() { 58 | ComponentScan componentScan = RootConfig.class.getAnnotation(ComponentScan.class); 59 | 60 | assertThat(componentScan, notNullValue()); 61 | } 62 | 63 | @Test 64 | void testRootConfigComponentScanPackages() { 65 | ComponentScan componentScan = RootConfig.class.getAnnotation(ComponentScan.class); 66 | String[] packages = componentScan.basePackages(); 67 | if (packages.length == 0) { 68 | packages = componentScan.value(); 69 | } 70 | 71 | assertThat(packages, arrayContaining("com.bobocode")); 72 | } 73 | 74 | @Test 75 | void testRootConfigComponentScanFilters() { 76 | ComponentScan componentScan = RootConfig.class.getAnnotation(ComponentScan.class); 77 | Filter[] filters = componentScan.excludeFilters(); 78 | List filteredClasses = getFilteredClasses(filters); 79 | 80 | assertThat(filters, arrayWithSize(2)); 81 | assertThat(filters[0].type(), equalTo(FilterType.ANNOTATION)); 82 | assertThat(filters[1].type(), equalTo(FilterType.ANNOTATION)); 83 | assertThat(filteredClasses, containsInAnyOrder(EnableWebMvc.class, Controller.class)); 84 | } 85 | 86 | private List getFilteredClasses(Filter[] filters) { 87 | return Stream.of(filters).flatMap(filter -> Stream.of(filter.value())).collect(Collectors.toList()); 88 | } 89 | 90 | @Test 91 | void testWebConfigIsMarkedAsConfiguration() { 92 | Configuration configuration = WebConfig.class.getAnnotation(Configuration.class); 93 | 94 | assertThat(configuration, notNullValue()); 95 | } 96 | 97 | @Test 98 | void testWebConfigEnablesComponentScan() { 99 | ComponentScan componentScan = WebConfig.class.getAnnotation(ComponentScan.class); 100 | 101 | assertThat(componentScan, notNullValue()); 102 | } 103 | 104 | @Test 105 | void testWebConfigComponentScanPackages() { 106 | ComponentScan componentScan = WebConfig.class.getAnnotation(ComponentScan.class); 107 | 108 | assertThat(componentScan.basePackages(), arrayContaining("com.bobocode.web")); 109 | } 110 | 111 | @Test 112 | void testWebConfigEnablesWebMvc() { 113 | EnableWebMvc enableWebMvc = WebConfig.class.getAnnotation(EnableWebMvc.class); 114 | 115 | assertThat(enableWebMvc, notNullValue()); 116 | } 117 | 118 | @Test 119 | void testWelcomeControllerIsMarkedAsController() { 120 | Controller controller = WelcomeController.class.getAnnotation(Controller.class); 121 | 122 | assertThat(controller, notNullValue()); 123 | } 124 | 125 | @Test 126 | void testWelcomeControllerMethodIsMarkedAsGetMethod() throws NoSuchMethodException { 127 | GetMapping getMapping = WelcomeController.class.getDeclaredMethod("welcome").getAnnotation(GetMapping.class); 128 | 129 | assertThat(getMapping, notNullValue()); 130 | } 131 | 132 | @Test 133 | void testWelcomeControllerMethodMapping() throws NoSuchMethodException { 134 | GetMapping getMapping = WelcomeController.class.getDeclaredMethod("welcome").getAnnotation(GetMapping.class); 135 | 136 | assertThat(getMapping.value(), arrayContaining("/welcome")); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /inter-bean-dependencies/README.MD: -------------------------------------------------------------------------------- 1 | # Inter-bean dependencies exercise :muscle: 2 | Improve your *Spring ApplicationContext* Java configuration skills 3 | ### Task 4 | The task is to **refactor Java configuration class `RootConfig`, in order to properly use Inter-bean dependencies.** 5 | Your job is to follow the instructions in the *todo* section and **implement 6 | a proper configuration.** 7 | 8 | To verify your configuration, run `RootConfigContextTest.java` and `RootConfigHacksTest.java` 9 | 10 | 11 | ### Pre-conditions :heavy_exclamation_mark: 12 | You're supposed to be familiar with *Spring IoC* and *Dependency injection* and Proxy pattern 13 | 14 | ### How to start :question: 15 | * Just clone the repository and start implementing the **todo** section, verify your changes by running tests 16 | * If you don't have enough knowledge about this domain, check out the [links below](#related-materials-information_source) 17 | * Don't worry if you got stuck, checkout the **exercise/completed** branch and see the final implementation 18 | 19 | ### Related materials :information_source: 20 | * [Spring @Configuration vs @Component](https://youtu.be/KWvTgqtA_HA) 21 | * [Injecting Inter-bean Dependencies](https://docs.spring.io/spring/docs/5.1.9.RELEASE/spring-framework-reference/core.html#beans-java-injecting-dependencies) 22 | 23 | -------------------------------------------------------------------------------- /inter-bean-dependencies/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-framework-exercises 7 | com.bobocode 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | bean-configuration-exercise 13 | 14 | 15 | 16 | com.bobocode 17 | spring-framework-exercises-util 18 | 1.0-SNAPSHOT 19 | 20 | 21 | -------------------------------------------------------------------------------- /inter-bean-dependencies/src/main/java/com/bobocode/config/RootConfig.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.config; 2 | 3 | import com.bobocode.TestDataGenerator; 4 | import com.bobocode.dao.AccountDao; 5 | import com.bobocode.dao.impl.FakeAccountDao; 6 | import com.bobocode.service.AccountService; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * todo: Refactor {@link RootConfig} in order to user inter-bean dependencies properly. 12 | */ 13 | @Component 14 | public final class RootConfig { 15 | 16 | @Bean 17 | public AccountService accountService() { 18 | return new AccountService(fakeAccountDao()); 19 | } 20 | 21 | @Bean 22 | public final AccountDao fakeAccountDao() { 23 | return new FakeAccountDao(dataGenerator()); 24 | } 25 | 26 | @Bean 27 | private TestDataGenerator dataGenerator() { 28 | return new TestDataGenerator(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /inter-bean-dependencies/src/main/java/com/bobocode/dao/AccountDao.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.dao; 2 | 3 | import com.bobocode.model.Account; 4 | 5 | public interface AccountDao { 6 | Account findById(Long id); 7 | } 8 | -------------------------------------------------------------------------------- /inter-bean-dependencies/src/main/java/com/bobocode/dao/impl/FakeAccountDao.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.dao.impl; 2 | 3 | import com.bobocode.TestDataGenerator; 4 | import com.bobocode.dao.AccountDao; 5 | import com.bobocode.model.Account; 6 | import lombok.Getter; 7 | import lombok.RequiredArgsConstructor; 8 | 9 | @Getter 10 | @RequiredArgsConstructor 11 | public class FakeAccountDao implements AccountDao { 12 | private final TestDataGenerator dataGenerator; 13 | 14 | @Override 15 | public Account findById(Long id) { 16 | Account account = dataGenerator.generateAccount(); 17 | account.setId(id); 18 | return account; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /inter-bean-dependencies/src/main/java/com/bobocode/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.service; 2 | 3 | import com.bobocode.dao.AccountDao; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | @Getter 8 | @RequiredArgsConstructor 9 | public class AccountService { 10 | private final AccountDao accountDao; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /inter-bean-dependencies/src/test/java/com/bobocode/RootConfigContextTest.java: -------------------------------------------------------------------------------- 1 | package com.bobocode; 2 | 3 | import com.bobocode.config.RootConfig; 4 | import com.bobocode.dao.impl.FakeAccountDao; 5 | import com.bobocode.service.AccountService; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | 13 | @SpringJUnitConfig(classes = RootConfig.class) 14 | class RootConfigContextTest { 15 | 16 | @Autowired 17 | private AccountService accountService; 18 | 19 | @Autowired 20 | private FakeAccountDao accountDao; 21 | 22 | @Autowired 23 | private TestDataGenerator dataGenerator; 24 | 25 | @Test 26 | void dataGeneratorShouldHaveScopeSingleton() { 27 | assertThat(accountDao.getDataGenerator()).isEqualTo(dataGenerator); 28 | } 29 | 30 | @Test 31 | void accountDaoShouldHaveScopeSingleton() { 32 | assertThat(accountService.getAccountDao()).isEqualTo(accountDao); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /inter-bean-dependencies/src/test/java/com/bobocode/RootConfigHacksTest.java: -------------------------------------------------------------------------------- 1 | package com.bobocode; 2 | 3 | import com.bobocode.config.RootConfig; 4 | import com.bobocode.dao.impl.FakeAccountDao; 5 | import com.bobocode.service.AccountService; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.context.annotation.Import; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.lang.annotation.Annotation; 13 | import java.lang.reflect.Method; 14 | import java.util.List; 15 | import java.util.stream.Collectors; 16 | import java.util.stream.Stream; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | class RootConfigHacksTest { 21 | 22 | @Test 23 | void rootConfigShouldNotUseComponentScan() { 24 | ComponentScan componentScan = RootConfig.class.getAnnotation(ComponentScan.class); 25 | 26 | assertThat(componentScan).isNull(); 27 | } 28 | 29 | @Test 30 | void rootConfigShouldNotImportOtherConfigs() { 31 | Import importAnnotation = RootConfig.class.getAnnotation(Import.class); 32 | 33 | assertThat(importAnnotation).isNull(); 34 | } 35 | 36 | @Test 37 | void accountServiceBeanShouldBeConfiguredExplicitly() { 38 | Annotation[] annotations = AccountService.class.getAnnotations(); 39 | List annotationClasses = Stream.of(annotations).map(Annotation::annotationType).collect(Collectors.toList()); 40 | 41 | assertThat(annotationClasses).doesNotContain(Component.class, Service.class); 42 | } 43 | 44 | @Test 45 | void fakeAccountDaoBeanShouldBeConfiguredExplicitly() { 46 | Annotation[] annotations = FakeAccountDao.class.getAnnotations(); 47 | List annotationClasses = Stream.of(annotations).map(Annotation::annotationType).collect(Collectors.toList()); 48 | 49 | assertThat(annotationClasses).doesNotContain(Component.class, Service.class); 50 | } 51 | 52 | @Test 53 | void rootConfigShouldUseInterBeanDependencies() { 54 | Method[] methods = RootConfig.class.getDeclaredMethods(); 55 | 56 | assertThat(methods).noneMatch(method -> method.getParameterCount() > 0); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.bobocode 8 | spring-framework-exercises 9 | 1.0-SNAPSHOT 10 | 11 | hello-application-context 12 | spring-framework-exercises-model 13 | spring-framework-exercises-util 14 | hello-spring-mvc 15 | account-jsp 16 | account-rest-api 17 | transactional-user-service 18 | inter-bean-dependencies 19 | 20 | pom 21 | 22 | 23 | 11 24 | 11 25 | 26 | 27 | 28 | 29 | org.projectlombok 30 | lombok 31 | 1.18.0 32 | 33 | 34 | org.springframework 35 | spring-context 36 | 5.0.7.RELEASE 37 | 38 | 39 | org.springframework 40 | spring-test 41 | 5.0.7.RELEASE 42 | 43 | 44 | javax.annotation 45 | javax.annotation-api 46 | 1.3.2 47 | 48 | 49 | org.junit.jupiter 50 | junit-jupiter-engine 51 | 5.2.0 52 | test 53 | 54 | 55 | org.junit.platform 56 | junit-platform-launcher 57 | 1.3.1 58 | test 59 | 60 | 61 | com.jayway.jsonpath 62 | json-path 63 | 2.3.0 64 | 65 | 66 | com.jayway.jsonpath 67 | json-path-assert 68 | 2.3.0 69 | 70 | 71 | 72 | org.hamcrest 73 | hamcrest-all 74 | 1.3 75 | test 76 | 77 | 78 | org.slf4j 79 | slf4j-simple 80 | 1.7.24 81 | 82 | 83 | org.assertj 84 | assertj-core 85 | 3.13.2 86 | test 87 | 88 | 89 | 90 | 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-surefire-plugin 95 | 2.22.0 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /spring-framework-exercises-model/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-framework-exercises 7 | com.bobocode 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | spring-framework-exercises-model 13 | 14 | 15 | 16 | org.hibernate.javax.persistence 17 | hibernate-jpa-2.1-api 18 | 1.0.2.Final 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /spring-framework-exercises-model/src/main/java/com/bobocode/model/Account.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.model; 2 | 3 | import lombok.*; 4 | 5 | import java.math.BigDecimal; 6 | import java.time.LocalDate; 7 | import java.time.LocalDateTime; 8 | 9 | @NoArgsConstructor 10 | @Getter 11 | @Setter 12 | @ToString 13 | @EqualsAndHashCode(of = "email") 14 | public class Account { 15 | private Long id; 16 | private String firstName; 17 | private String lastName; 18 | private String email; 19 | private LocalDate birthday; 20 | private Gender gender; 21 | private LocalDateTime creationTime; 22 | private BigDecimal balance = BigDecimal.ZERO; 23 | } 24 | -------------------------------------------------------------------------------- /spring-framework-exercises-model/src/main/java/com/bobocode/model/Gender.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.model; 2 | 3 | public enum Gender { 4 | MALE, 5 | FEMALE 6 | } 7 | -------------------------------------------------------------------------------- /spring-framework-exercises-model/src/main/java/com/bobocode/model/jpa/Role.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.model.jpa; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import lombok.ToString; 7 | 8 | import javax.persistence.*; 9 | import java.time.LocalDateTime; 10 | import java.util.Objects; 11 | 12 | @NoArgsConstructor 13 | @Getter 14 | @Setter 15 | @ToString(exclude = "user") 16 | @Entity 17 | @Table(name = "role") 18 | public class Role { 19 | @Id 20 | @GeneratedValue 21 | private Long id; 22 | 23 | @Enumerated(EnumType.STRING) 24 | @Column(name = "role_type") 25 | private RoleType roleType; 26 | 27 | @Column(name = "creation_date") 28 | private LocalDateTime creationDate = LocalDateTime.now(); 29 | 30 | @ManyToOne 31 | @JoinColumn(name = "user_id") 32 | private User user; 33 | 34 | public static Role valueOf(RoleType roleType) { 35 | return new Role(roleType); 36 | } 37 | 38 | private Role(RoleType roleType) { 39 | this.roleType = roleType; 40 | } 41 | 42 | @Override 43 | public boolean equals(Object o) { 44 | if (this == o) return true; 45 | if (!(o instanceof Role)) return false; 46 | 47 | Role role = (Role) o; 48 | 49 | return Objects.equals(id, role.id); 50 | } 51 | 52 | @Override 53 | public int hashCode() { 54 | return 31; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /spring-framework-exercises-model/src/main/java/com/bobocode/model/jpa/RoleType.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.model.jpa; 2 | 3 | public enum RoleType { 4 | USER, ADMIN, OPERATOR, CUSTOMER 5 | } 6 | -------------------------------------------------------------------------------- /spring-framework-exercises-model/src/main/java/com/bobocode/model/jpa/User.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.model.jpa; 2 | 3 | import lombok.*; 4 | 5 | import javax.persistence.*; 6 | import java.time.LocalDate; 7 | import java.util.HashSet; 8 | import java.util.Set; 9 | 10 | 11 | @NoArgsConstructor 12 | @Getter 13 | @Setter 14 | @ToString 15 | @EqualsAndHashCode(of = "email") 16 | @Entity 17 | @Table(name = "user") 18 | public class User { 19 | @Id 20 | @GeneratedValue 21 | private Long id; 22 | 23 | @Column(name = "first_name") 24 | private String firstName; 25 | 26 | @Column(name = "last_name") 27 | private String lastName; 28 | 29 | @Column(name = "email") 30 | private String email; 31 | 32 | @Column(name = "birthday") 33 | private LocalDate birthday; 34 | 35 | @Column(name = "creation_date") 36 | private LocalDate creationDate; 37 | 38 | @Setter(AccessLevel.PRIVATE) 39 | @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) 40 | private Set roles = new HashSet<>(); 41 | 42 | public void addRole(Role role) { 43 | roles.add(role); 44 | role.setUser(this); 45 | } 46 | 47 | public void addRoles(Set roles) { 48 | this.roles.addAll(roles); 49 | roles.forEach(role -> role.setUser(this)); 50 | } 51 | 52 | public void removeRole(Role role) { 53 | this.roles.remove(role); 54 | role.setUser(null); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /spring-framework-exercises-util/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-framework-exercises 7 | com.bobocode 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | spring-framework-exercises-util 13 | 14 | 15 | 16 | 17 | com.bobocode 18 | spring-framework-exercises-model 19 | 1.0-SNAPSHOT 20 | 21 | 22 | io.codearte.jfairy 23 | jfairy 24 | 0.5.7 25 | 26 | 27 | -------------------------------------------------------------------------------- /spring-framework-exercises-util/src/main/java/com/bobocode/TestDataGenerator.java: -------------------------------------------------------------------------------- 1 | package com.bobocode; 2 | 3 | import com.bobocode.model.Account; 4 | import com.bobocode.model.Gender; 5 | import com.bobocode.model.jpa.Role; 6 | import com.bobocode.model.jpa.RoleType; 7 | import com.bobocode.model.jpa.User; 8 | import io.codearte.jfairy.Fairy; 9 | import io.codearte.jfairy.producer.person.Person; 10 | 11 | import java.math.BigDecimal; 12 | import java.time.LocalDate; 13 | import java.time.LocalDateTime; 14 | import java.util.Random; 15 | import java.util.Set; 16 | import java.util.function.Predicate; 17 | import java.util.stream.Collectors; 18 | import java.util.stream.Stream; 19 | 20 | import static java.util.stream.Collectors.toSet; 21 | 22 | public class TestDataGenerator { 23 | public Account generateAccount(){ 24 | Fairy fairy = Fairy.create(); 25 | Person person = fairy.person(); 26 | Random random = new Random(); 27 | 28 | Account fakeAccount = new Account(); 29 | fakeAccount.setFirstName(person.getFirstName()); 30 | fakeAccount.setLastName(person.getLastName()); 31 | fakeAccount.setEmail(person.getEmail()); 32 | fakeAccount.setBirthday(LocalDate.of( 33 | person.getDateOfBirth().getYear(), 34 | person.getDateOfBirth().getMonthOfYear(), 35 | person.getDateOfBirth().getDayOfMonth())); 36 | fakeAccount.setGender(Gender.valueOf(person.getSex().name())); 37 | fakeAccount.setBalance(BigDecimal.valueOf(random.nextInt(200_000))); 38 | fakeAccount.setCreationTime(LocalDateTime.now()); 39 | 40 | return fakeAccount; 41 | } 42 | 43 | public User generateUser() { 44 | User user = generateUserOnly(); 45 | user.addRoles(generateRoleSet()); 46 | return user; 47 | } 48 | 49 | public User generateUser(RoleType... roleTypes) { 50 | Set roles = Stream.of(roleTypes).map(Role::valueOf).collect(Collectors.toSet()); 51 | User user = generateUserOnly(); 52 | user.addRoles(roles); 53 | return user; 54 | } 55 | 56 | private User generateUserOnly() { 57 | Fairy fairy = Fairy.create(); 58 | Person person = fairy.person(); 59 | 60 | User user = new User(); 61 | user.setFirstName(person.getFirstName()); 62 | user.setLastName(person.getLastName()); 63 | user.setEmail(person.getEmail()); 64 | user.setBirthday(LocalDate.of( 65 | person.getDateOfBirth().getYear(), 66 | person.getDateOfBirth().getMonthOfYear(), 67 | person.getDateOfBirth().getDayOfMonth())); 68 | user.setCreationDate(LocalDate.now()); 69 | return user; 70 | } 71 | 72 | public static Set generateRoleSet() { 73 | Random random = new Random(); 74 | Predicate randomPredicate = i -> random.nextBoolean(); 75 | 76 | return Stream.of(RoleType.values()) 77 | .filter(randomPredicate) 78 | .map(Role::valueOf) 79 | .collect(toSet()); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /transactional-user-service/README.MD: -------------------------------------------------------------------------------- 1 | # Transactional UserService exercise :muscle: 2 | Improve your *Spring ORM* and *Transaction manager* configuration skills 3 | ### Task 4 | This is a *Spring* application that consists of `UserService` and `UserDao` that is implemented using *JPA*. 5 | Your job is to **configure all required beans to use *JPA*** then **configure `PlatformTransactionManager`** and 6 | **enable declarative transaction management**. In order to complete the task, please **follow the instructions 7 | in the *todo* section** 8 | 9 | To verify your configuration, run `TransactionalUserServiceTest.java` :white_check_mark: 10 | 11 | 12 | ### Pre-conditions :heavy_exclamation_mark: 13 | You're supposed to be familiar with *Spring MVC* 14 | 15 | ### How to start :question: 16 | * Just clone the repository and start implementing the **todo** section, verify your changes by running tests 17 | * If you don't have enough knowledge about this domain, check out the [links below](#related-materials-information_source) 18 | * Don't worry if you got stuck, checkout the **exercise/completed** branch and see the final implementation 19 | 20 | ### Related materials :information_source: 21 | * [Spring JPA and Tx management](https://github.com/bobocode-projects/spring-framework-tutorial/tree/master/jpa-tx-management) 22 | 23 | -------------------------------------------------------------------------------- /transactional-user-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-framework-exercises 7 | com.bobocode 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | transactional-user-service 13 | 14 | 15 | 16 | org.springframework 17 | spring-orm 18 | 5.0.7.RELEASE 19 | 20 | 21 | com.h2database 22 | h2 23 | 1.4.197 24 | 25 | 26 | org.slf4j 27 | slf4j-simple 28 | 1.7.24 29 | 30 | 31 | org.hibernate.javax.persistence 32 | hibernate-jpa-2.1-api 33 | 1.0.2.Final 34 | 35 | 36 | org.hibernate 37 | hibernate-core 38 | 5.3.2.Final 39 | 40 | 41 | 42 | 43 | javax.xml.bind 44 | jaxb-api 45 | 2.2.11 46 | 47 | 48 | 49 | com.bobocode 50 | spring-framework-exercises-util 51 | 1.0-SNAPSHOT 52 | 53 | 54 | -------------------------------------------------------------------------------- /transactional-user-service/src/main/java/com/bobocode/config/JpaConfig.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.config; 2 | 3 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; 4 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; 5 | import org.springframework.orm.jpa.JpaVendorAdapter; 6 | import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 7 | import org.springframework.orm.jpa.vendor.Database; 8 | import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; 9 | 10 | import javax.sql.DataSource; 11 | 12 | /** 13 | * {@link JpaConfig} provides configuration required to create {@link javax.persistence.EntityManagerFactory}. In order 14 | * to configure {@link javax.persistence.EntityManagerFactory} Spring needs to configure two beans {@link DataSource} 15 | * and {@link JpaVendorAdapter}. 16 | *

17 | * todo: 1. Mark this class as spring config 18 | * todo: 2. Configure a bean of {@link DataSource} 19 | * todo: 3. Configure a bean of {@link JpaVendorAdapter} 20 | * todo: 4. Configure bean {@link javax.persistence.EntityManagerFactory} with name "entityManagerFactory" 21 | * 22 | */ 23 | public class JpaConfig { 24 | public DataSource dataSource() { 25 | return new EmbeddedDatabaseBuilder() 26 | .setType(EmbeddedDatabaseType.H2) 27 | .build(); 28 | } 29 | 30 | public JpaVendorAdapter jpaVendorAdapter() { 31 | HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter(); 32 | adapter.setDatabase(Database.H2); 33 | adapter.setShowSql(true); 34 | adapter.setGenerateDdl(true); // this sets hibernate.hbm2ddl.auto=update (Hibernate will generate db tables) 35 | return adapter; 36 | } 37 | 38 | public LocalContainerEntityManagerFactoryBean localContainerEMF(DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) { 39 | LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean(); 40 | emf.setDataSource(dataSource); 41 | emf.setJpaVendorAdapter(jpaVendorAdapter); 42 | // todo: 5. Configure package "com.bobocode.model" to scan for JPA entities 43 | return emf; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /transactional-user-service/src/main/java/com/bobocode/config/RootConfig.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.config; 2 | 3 | import org.springframework.transaction.PlatformTransactionManager; 4 | 5 | /** 6 | * This class provides root application configuration. It scans for all available beans and enables declarative 7 | * transaction management. 8 | *

9 | * todo: 1. Mark this class as config 10 | * todo: 2. Enable component scanning for package "com.bobocode" 11 | * todo: 3. Configure JPA {@link PlatformTransactionManager} with bean name "transactionManager" 12 | * todo: 4. Enable declarative transaction management 13 | */ 14 | public class RootConfig { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /transactional-user-service/src/main/java/com/bobocode/dao/UserDao.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.dao; 2 | 3 | import com.bobocode.model.jpa.User; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * User Data Access Object (DAO) API 9 | */ 10 | public interface UserDao { 11 | List findAll(); 12 | 13 | User findById(long id); 14 | 15 | void save(User user); 16 | } 17 | -------------------------------------------------------------------------------- /transactional-user-service/src/main/java/com/bobocode/dao/impl/JpaUserDao.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.dao.impl; 2 | 3 | import com.bobocode.dao.UserDao; 4 | import com.bobocode.model.jpa.User; 5 | import org.springframework.stereotype.Repository; 6 | import org.springframework.transaction.annotation.Transactional; 7 | 8 | import javax.persistence.EntityManager; 9 | import javax.persistence.PersistenceContext; 10 | import java.util.List; 11 | 12 | /** 13 | * This class implements {@link UserDao} using JPA. 14 | *

15 | * todo: 1. Configure {@link JpaUserDao} bean as Spring Repository with name "userDao" 16 | * todo: 2. Enable transaction management on class level 17 | * todo: 3. Inject {@link EntityManager} using @{@link PersistenceContext} annotation 18 | */ 19 | public class JpaUserDao implements UserDao { 20 | private EntityManager entityManager; 21 | 22 | @Override 23 | public List findAll() { 24 | return entityManager.createQuery("select u from User u", User.class).getResultList(); 25 | } 26 | 27 | @Override 28 | public User findById(long id) { 29 | return entityManager.find(User.class, id); 30 | } 31 | 32 | @Override 33 | public void save(User user) { 34 | entityManager.persist(user); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /transactional-user-service/src/main/java/com/bobocode/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.bobocode.service; 2 | 3 | import com.bobocode.dao.UserDao; 4 | import com.bobocode.model.jpa.Role; 5 | import com.bobocode.model.jpa.RoleType; 6 | import com.bobocode.model.jpa.User; 7 | 8 | import java.util.List; 9 | 10 | import static java.util.stream.Collectors.toList; 11 | 12 | /** 13 | * This class proovides {@link User} related service logic. 14 | *

15 | * todo: 1. Configure {@link UserService} bean as spring service 16 | * todo: 2. Inject {@link UserDao} using constructor-based injection 17 | * todo: 3. Enable transaction management on class level 18 | * todo: 4. Configure {@link UserService#getAll()} as read-only method 19 | * todo: 4. Configure {@link UserService#getAllAdmins()} as read-only method 20 | */ 21 | public class UserService { 22 | private UserDao userDao; 23 | 24 | public void save(User user) { 25 | userDao.save(user); 26 | } 27 | 28 | public List getAll() { 29 | return userDao.findAll(); 30 | } 31 | 32 | public List getAllAdmins() { 33 | throw new UnsupportedOperationException("Don't be lazy and implement the method"); 34 | } 35 | 36 | public void addRole(Long userId, RoleType roleType) { 37 | User user = userDao.findById(userId); 38 | Role role = Role.valueOf(roleType); 39 | user.addRole(role); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /transactional-user-service/src/test/java/com/bobocode/TransactionalUserServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.bobocode; 2 | 3 | import com.bobocode.config.RootConfig; 4 | import com.bobocode.dao.UserDao; 5 | import com.bobocode.dao.impl.JpaUserDao; 6 | import com.bobocode.model.jpa.Role; 7 | import com.bobocode.model.jpa.RoleType; 8 | import com.bobocode.model.jpa.User; 9 | import com.bobocode.service.UserService; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.context.ApplicationContext; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.Configuration; 15 | import org.springframework.stereotype.Repository; 16 | import org.springframework.stereotype.Service; 17 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 18 | import org.springframework.transaction.PlatformTransactionManager; 19 | import org.springframework.transaction.annotation.Transactional; 20 | 21 | import javax.persistence.EntityManagerFactory; 22 | import java.util.List; 23 | import java.util.stream.Stream; 24 | 25 | import static java.util.stream.Collectors.toList; 26 | import static org.hamcrest.MatcherAssert.assertThat; 27 | import static org.hamcrest.Matchers.contains; 28 | import static org.hamcrest.Matchers.containsInAnyOrder; 29 | import static org.hamcrest.Matchers.hasProperty; 30 | import static org.hamcrest.Matchers.is; 31 | import static org.hamcrest.core.IsCollectionContaining.hasItem; 32 | import static org.hamcrest.core.IsNull.notNullValue; 33 | 34 | @SpringJUnitConfig(RootConfig.class) 35 | @Transactional 36 | class TransactionalUserServiceTest { 37 | @Configuration 38 | static class TestConfig { 39 | @Bean 40 | TestDataGenerator dataGenerator() { 41 | return new TestDataGenerator(); 42 | } 43 | } 44 | 45 | @Autowired 46 | private ApplicationContext applicationContext; 47 | @Autowired 48 | private UserService userService; 49 | @Autowired 50 | private UserDao userDao; 51 | @Autowired 52 | private TestDataGenerator dataGenerator; 53 | 54 | @Test 55 | void testTxManagerBeanName() { 56 | PlatformTransactionManager transactionManager = applicationContext.getBean(PlatformTransactionManager.class, "transactionManager"); 57 | 58 | assertThat(transactionManager, notNullValue()); 59 | } 60 | 61 | @Test 62 | void testUserDaoBeanName() { 63 | UserDao userDao = applicationContext.getBean(UserDao.class, "userDao"); 64 | 65 | assertThat(userDao, notNullValue()); 66 | } 67 | 68 | @Test 69 | void testEntityManagerFactoryBeanName() { 70 | EntityManagerFactory entityManagerFactory = applicationContext.getBean(EntityManagerFactory.class, "entityManagerFactory"); 71 | 72 | assertThat(entityManagerFactory, notNullValue()); 73 | } 74 | 75 | @Test 76 | void testUserServiceIsMarkedAsService() { 77 | Service service = UserService.class.getAnnotation(Service.class); 78 | 79 | assertThat(service, notNullValue()); 80 | } 81 | 82 | @Test 83 | void testUserDaoIsMarkedAsRepository() { 84 | Repository repository = JpaUserDao.class.getAnnotation(Repository.class); 85 | 86 | assertThat(repository, notNullValue()); 87 | } 88 | 89 | @Test 90 | void testUserServiceIsTransactional() { 91 | Transactional transactional = UserService.class.getAnnotation(Transactional.class); 92 | 93 | assertThat(transactional, notNullValue()); 94 | } 95 | 96 | @Test 97 | void testUserServiceGetAllIsReadOnly() throws NoSuchMethodException { 98 | Transactional transactional = UserService.class.getDeclaredMethod("getAll").getAnnotation(Transactional.class); 99 | 100 | assertThat(transactional.readOnly(), is(true)); 101 | } 102 | 103 | @Test 104 | void testUserServiceGetAllAdminsIsReadOnly() throws NoSuchMethodException { 105 | Transactional transactional = UserService.class.getDeclaredMethod("getAllAdmins").getAnnotation(Transactional.class); 106 | 107 | assertThat(transactional.readOnly(), is(true)); 108 | } 109 | 110 | @Test 111 | void testUserDaoIsTransactional() { 112 | Transactional transactional = JpaUserDao.class.getAnnotation(Transactional.class); 113 | 114 | assertThat(transactional, notNullValue()); 115 | } 116 | 117 | @Test 118 | void testSaveUser() { 119 | User user = dataGenerator.generateUser(); 120 | userService.save(user); 121 | 122 | assertThat(userDao.findAll(), hasItem(user)); 123 | } 124 | 125 | @Test 126 | void testGetAllUsers() { 127 | List userList = generateUserList(10); 128 | userList.forEach(userService::save); 129 | 130 | List users = userService.getAll(); 131 | assertThat(users, containsInAnyOrder(userList.toArray())); 132 | } 133 | 134 | private List generateUserList(int size) { 135 | return Stream.generate(dataGenerator::generateUser) 136 | .limit(size) 137 | .collect(toList()); 138 | } 139 | 140 | @Test 141 | void testGetAllAdmins() { 142 | List userList = generateUserList(20); 143 | userList.forEach(userService::save); 144 | 145 | List admins = userService.getAllAdmins(); 146 | 147 | assertThat(admins, containsInAnyOrder(findAdmins(userList).toArray())); 148 | } 149 | 150 | private List findAdmins(List users) { 151 | return users.stream() 152 | .filter(user -> user.getRoles().stream() 153 | .map(Role::getRoleType) 154 | .anyMatch(roleType -> roleType.equals(RoleType.ADMIN))) 155 | .collect(toList()); 156 | 157 | } 158 | 159 | @Test 160 | void testAddNewRole() { 161 | User user = dataGenerator.generateUser(RoleType.USER); 162 | userService.save(user); 163 | 164 | userService.addRole(user.getId(), RoleType.ADMIN); 165 | 166 | User loadedUser = userDao.findById(user.getId()); 167 | assertThat(loadedUser.getRoles(), contains( 168 | hasProperty("roleType", is(RoleType.USER)), 169 | hasProperty("roleType", is(RoleType.ADMIN))) 170 | ); 171 | } 172 | } 173 | --------------------------------------------------------------------------------