Skip to content

Commit ec77c48

Browse files
shryhustboychuk
authored andcommitted
GP-83 migrate account-rest-api
1 parent bbbd710 commit ec77c48

File tree

11 files changed

+363
-0
lines changed

11 files changed

+363
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# <img src="https://raw.githubusercontent.com/bobocode-projects/resources/master/image/logo_transparent_background.png" height=50/>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)<img src="https://raw.githubusercontent.com/bobocode-projects/resources/master/image/logo_transparent_background.png" height=20/>
21+
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<parent>
6+
<artifactId>3-0-spring-framework</artifactId>
7+
<groupId>com.bobocode</groupId>
8+
<version>1.0-SNAPSHOT</version>
9+
</parent>
10+
<modelVersion>4.0.0</modelVersion>
11+
12+
<artifactId>3-2-1-account-rest-api</artifactId>
13+
<packaging>war</packaging>
14+
15+
<dependencies>
16+
<dependency>
17+
<groupId>org.springframework</groupId>
18+
<artifactId>spring-webmvc</artifactId>
19+
<version>5.0.7.RELEASE</version>
20+
</dependency>
21+
<dependency>
22+
<groupId>javax.servlet</groupId>
23+
<artifactId>javax.servlet-api</artifactId>
24+
<version>4.0.1</version>
25+
<scope>provided</scope>
26+
</dependency>
27+
<dependency>
28+
<groupId>com.bobocode</groupId>
29+
<artifactId>spring-framework-exercises-util</artifactId>
30+
<version>1.0-SNAPSHOT</version>
31+
</dependency>
32+
<dependency>
33+
<groupId>com.fasterxml.jackson.core</groupId>
34+
<artifactId>jackson-core</artifactId>
35+
<version>2.9.7</version>
36+
</dependency>
37+
<dependency>
38+
<groupId>com.fasterxml.jackson.core</groupId>
39+
<artifactId>jackson-databind</artifactId>
40+
<version>2.9.7</version>
41+
</dependency>
42+
</dependencies>
43+
44+
<build>
45+
<plugins>
46+
47+
<plugin>
48+
<groupId>org.apache.maven.plugins</groupId>
49+
<artifactId>maven-war-plugin</artifactId>
50+
<version>2.6</version>
51+
<configuration>
52+
<failOnMissingWebXml>false</failOnMissingWebXml>
53+
</configuration>
54+
</plugin>
55+
56+
</plugins>
57+
</build>
58+
59+
60+
</project>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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+
* <p>
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+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.bobocode.config;
2+
3+
/**
4+
* This class provides web (servlet) related configuration.
5+
* <p>
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+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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<Account> findAll();
9+
10+
Account findById(long id);
11+
12+
Account save(Account account);
13+
14+
void remove(Account account);
15+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
* <p>
15+
* todo: 1. Configure a component with name "accountDao"
16+
*/
17+
public class InMemoryAccountDao implements AccountDao {
18+
private Map<Long, Account> accountMap = new HashMap<>();
19+
private long idSequence = 1L;
20+
21+
@Override
22+
public List<Account> 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+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.bobocode.exception;
2+
3+
public class EntityNotFountException extends RuntimeException {
4+
public EntityNotFountException(String message) {
5+
super(message);
6+
}
7+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.bobocode.web.controller;
2+
3+
import com.bobocode.dao.AccountDao;
4+
5+
/**
6+
* <p>
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+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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+
}

0 commit comments

Comments
 (0)