Last active
August 29, 2015 14:24
-
-
Save arawn/57ff46f153898a565d61 to your computer and use it in GitHub Desktop.
Hands On Lab
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.ArrayList; | |
import java.util.Date; | |
import java.util.LinkedHashMap; | |
import java.util.List; | |
import java.util.Locale; | |
import java.util.Map; | |
import javax.validation.Valid; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.context.MessageSource; | |
import org.springframework.http.HttpStatus; | |
import org.springframework.http.ResponseEntity; | |
import org.springframework.validation.BindException; | |
import org.springframework.web.bind.annotation.ExceptionHandler; | |
import org.springframework.web.bind.annotation.PathVariable; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
import org.springframework.web.bind.annotation.RequestMethod; | |
import org.springframework.web.bind.annotation.RestController; | |
import demo.board.Board; | |
import demo.board.Post; | |
import demo.board.module.BoardService; | |
import demo.board.module.PostForm; | |
import demo.board.module.ResourceNotFoundException; | |
@RestController | |
@RequestMapping("/board") | |
public class BoardController { | |
private BoardService boardService; | |
private MessageSource messageSource; | |
@RequestMapping("/{boardname}/info") | |
public ResponseEntity<Board> boardInfo(@PathVariable String boardname) { | |
Board board = boardService.findBoard(boardname); | |
return ResponseEntity.ok(board); | |
} | |
@RequestMapping(value = "/{boardname}", method = { RequestMethod.GET, RequestMethod.HEAD }) | |
public ResponseEntity<List<Post>> listPosts(@PathVariable String boardname) { | |
List<Post> posts = boardService.findPosts(boardname); | |
return ResponseEntity.ok(posts); | |
} | |
@RequestMapping(value = "/{boardname}", method = { RequestMethod.POST }) | |
public ResponseEntity<Post> createPost(@PathVariable String boardname, @Valid PostForm postForm) { | |
Post post = boardService.writePost(boardname, postForm); | |
return ResponseEntity.ok(post); | |
} | |
@RequestMapping(value = "/{boardname}/{postId}", method = { RequestMethod.PUT }) | |
public ResponseEntity<Post> updatePost(@PathVariable Long postId, PostForm postForm) { | |
Post post = boardService.editPost(postId, postForm); | |
return ResponseEntity.ok(post); | |
} | |
@RequestMapping(value = "/{boardname}/{postId}", method = { RequestMethod.DELETE }) | |
public ResponseEntity<Post> deletePost(@PathVariable Long postId) { | |
Post post = boardService.erasePost(postId); | |
return ResponseEntity.ok(post); | |
} | |
@ExceptionHandler(ResourceNotFoundException.class) | |
public ResponseEntity<Map<String, Object>> handleResourceNotFoundException(ResourceNotFoundException e, Locale locale) { | |
Map<String, Object> body = new LinkedHashMap<>(); | |
body.put("timestamp", new Date()); | |
body.put("status", e.getStatus().value()); | |
body.put("error", e.getError()); | |
body.put("message", messageSource.getMessage(e.getCode(), e.getArgs(), "No message available", locale)); | |
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); | |
} | |
@ExceptionHandler(BindException.class) | |
public ResponseEntity<Map<String, Object>> handleBindException(BindException e, Locale locale) { | |
Map<String, Object> body = new LinkedHashMap<>(); | |
body.put("timestamp", new Date()); | |
body.put("status", HttpStatus.BAD_REQUEST.value()); | |
body.put("error", HttpStatus.BAD_REQUEST.getReasonPhrase()); | |
body.put("message", messageSource.getMessage("error.BindException", null, "No message available", locale)); | |
List<String> errors = new ArrayList<>(); | |
e.getAllErrors().forEach(error -> { | |
errors.add(messageSource.getMessage(error, locale)); | |
}); | |
body.put("errors", errors); | |
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body); | |
} | |
@Autowired | |
public void setBoardService(BoardService boardService) { | |
this.boardService = boardService; | |
} | |
@Autowired | |
public void setMessageSource(MessageSource messageSource) { | |
this.messageSource = messageSource; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.net.URI; | |
import java.util.Arrays; | |
import java.util.List; | |
import java.util.UUID; | |
import org.hamcrest.Matchers; | |
import org.junit.Before; | |
import org.junit.Test; | |
import org.junit.runner.RunWith; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.beans.factory.annotation.Value; | |
import org.springframework.boot.test.SpringApplicationConfiguration; | |
import org.springframework.boot.test.TestRestTemplate; | |
import org.springframework.boot.test.WebIntegrationTest; | |
import org.springframework.http.HttpHeaders; | |
import org.springframework.http.HttpMethod; | |
import org.springframework.http.HttpStatus; | |
import org.springframework.http.MediaType; | |
import org.springframework.http.RequestEntity; | |
import org.springframework.http.ResponseEntity; | |
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; | |
import org.springframework.test.util.MatcherAssertionErrors; | |
import org.springframework.util.LinkedMultiValueMap; | |
import org.springframework.util.MultiValueMap; | |
import org.springframework.web.util.UriComponentsBuilder; | |
import com.fasterxml.jackson.databind.JsonNode; | |
import com.fasterxml.jackson.databind.node.ArrayNode; | |
import demo.HandsonlabApplication; | |
import demo.board.Post; | |
import demo.board.module.BoardNotFoundException; | |
import demo.board.module.BoardService; | |
import demo.board.module.PostForm; | |
import demo.board.module.PostNotFoundException; | |
import demo.board.module.ResourceNotFoundException; | |
@RunWith(SpringJUnit4ClassRunner.class) | |
@SpringApplicationConfiguration(classes = HandsonlabApplication.class) | |
@WebIntegrationTest(randomPort = true) | |
public class BoardControllerTest { | |
private static final String PATH_BOARDINFO = "/board/{boardname}/info"; | |
private static final String PATH_POST = "/board/{boardname}"; | |
private static final String PATH_UPDATE_POST = "/board/{boardname}/{postId}"; | |
private static final String TEST_BOARDNAME = "notice"; | |
@Value("${local.server.port}") | |
private int serverPort; | |
@Autowired | |
private BoardService boardService; | |
private TestRestTemplate restTemplate; | |
@Before | |
public void setUp() { | |
this.restTemplate = new TestRestTemplate(); | |
this.restTemplate.getInterceptors().add((request, body, execution) -> { | |
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); | |
return execution.execute(request, body); | |
}); | |
} | |
@Test | |
public void 공지사항_게시판_정보_확인() { | |
UriComponentsBuilder uriBuilder = uriBuilderFromPath(PATH_BOARDINFO); | |
ResponseEntity<JsonNode> resposne = restTemplate.getForEntity(uriBuilder.buildAndExpand(TEST_BOARDNAME).toUri(), JsonNode.class); | |
MatcherAssertionErrors.assertThat(resposne.getStatusCode(), Matchers.is(HttpStatus.OK)); | |
MatcherAssertionErrors.assertThat(resposne.getBody().path("name").asText(), Matchers.equalTo(TEST_BOARDNAME)); | |
} | |
@Test | |
public void 게시판이_없으면_BoardNotFoundException_발생() { | |
UriComponentsBuilder uriBuilder = uriBuilderFromPath(PATH_BOARDINFO); | |
ResponseEntity<JsonNode> resposne = restTemplate.getForEntity(uriBuilder.buildAndExpand("error").toUri(), JsonNode.class); | |
MatcherAssertionErrors.assertThat(resposne.getStatusCode(), Matchers.equalTo(HttpStatus.NOT_FOUND)); | |
assertNotFound(resposne.getBody(), new BoardNotFoundException(TEST_BOARDNAME)); | |
} | |
@Test | |
public void 게시물_목록_요청() { | |
boardService.writePost(TEST_BOARDNAME, new PostForm("tester", "Hi~ 1", "No Content")); | |
boardService.writePost(TEST_BOARDNAME, new PostForm("tester", "Hi~ 2", "No Content")); | |
List<Post> posts = boardService.findPosts(TEST_BOARDNAME); | |
UriComponentsBuilder uriBuilder = uriBuilderFromPath(PATH_POST); | |
ResponseEntity<ArrayNode> resposne = restTemplate.getForEntity(uriBuilder.buildAndExpand(TEST_BOARDNAME).toUri(), ArrayNode.class); | |
MatcherAssertionErrors.assertThat(resposne.getStatusCode(), Matchers.is(HttpStatus.OK)); | |
MatcherAssertionErrors.assertThat(resposne.getBody().size(), Matchers.is(posts.size())); | |
} | |
@Test | |
public void 게시물_쓰기_요청() { | |
URI uriCreatePost = uriBuilderFromPath(PATH_POST).buildAndExpand(TEST_BOARDNAME).toUri(); | |
HttpHeaders headers = new HttpHeaders(); | |
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); | |
MultiValueMap<String, Object> form = new LinkedMultiValueMap<>(); | |
form.add("author", "tester"); | |
form.add("title", UUID.randomUUID().toString()); | |
form.add("content", UUID.randomUUID().toString()); | |
ResponseEntity<JsonNode> resposne = restTemplate.exchange(new RequestEntity<>(form, headers, HttpMethod.POST, uriCreatePost), JsonNode.class); | |
MatcherAssertionErrors.assertThat(resposne.getStatusCode(), Matchers.is(HttpStatus.OK)); | |
assertPost(form, resposne.getBody()); | |
} | |
@Test | |
public void 게시물_수정_요청() { | |
Post origin = boardService.writePost(TEST_BOARDNAME, new PostForm("tester", "Hi~", "No Content")); | |
URI uriUpdatePost = uriBuilderFromPath(PATH_UPDATE_POST).buildAndExpand(TEST_BOARDNAME, origin.getId()).toUri(); | |
HttpHeaders headers = new HttpHeaders(); | |
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); | |
MultiValueMap<String, Object> form = new LinkedMultiValueMap<>(); | |
form.add("author", "tester"); | |
form.add("title", UUID.randomUUID().toString()); | |
form.add("content", UUID.randomUUID().toString()); | |
ResponseEntity<JsonNode> resposne = restTemplate.exchange(new RequestEntity<>(form, headers, HttpMethod.PUT, uriUpdatePost), JsonNode.class); | |
MatcherAssertionErrors.assertThat(resposne.getStatusCode(), Matchers.is(HttpStatus.OK)); | |
assertPost(form, resposne.getBody()); | |
} | |
private void assertPost(MultiValueMap<String, Object> form, JsonNode body) { | |
MatcherAssertionErrors.assertThat(body.path("author").asText(), Matchers.equalTo(form.get("author").get(0))); | |
MatcherAssertionErrors.assertThat(body.path("title").asText(), Matchers.equalTo(form.get("title").get(0))); | |
MatcherAssertionErrors.assertThat(body.path("content").asText(), Matchers.equalTo(form.get("content").get(0))); | |
} | |
@Test | |
public void 수정할_게시물이_없으면_PostNotFoundException_발생() { | |
URI uriUpdatePost = uriBuilderFromPath(PATH_UPDATE_POST).buildAndExpand(TEST_BOARDNAME, Integer.MAX_VALUE).toUri(); | |
ResponseEntity<JsonNode> resposne = restTemplate.exchange(new RequestEntity<>(HttpMethod.PUT, uriUpdatePost), JsonNode.class); | |
MatcherAssertionErrors.assertThat(resposne.getStatusCode(), Matchers.equalTo(HttpStatus.NOT_FOUND)); | |
assertNotFound(resposne.getBody(), new PostNotFoundException(Integer.MAX_VALUE)); | |
} | |
private void assertNotFound(JsonNode body, ResourceNotFoundException resourceNotFoundException) { | |
MatcherAssertionErrors.assertThat(body.path("status").asInt(), Matchers.is(resourceNotFoundException.getStatus().value())); | |
MatcherAssertionErrors.assertThat(body.path("error").asText(), Matchers.equalTo(resourceNotFoundException.getError())); | |
MatcherAssertionErrors.assertThat(body.has("message"), Matchers.is(true)); | |
} | |
private UriComponentsBuilder uriBuilderFromPath(String path) { | |
return UriComponentsBuilder.fromPath(path).scheme("http").host("localhost").port(serverPort); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.ArrayList; | |
import java.util.LinkedHashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Objects; | |
import java.util.concurrent.atomic.AtomicLong; | |
import java.util.stream.Collectors; | |
import javax.annotation.PostConstruct; | |
import org.springframework.stereotype.Service; | |
import demo.board.Board; | |
import demo.board.Post; | |
@Service | |
public class BoardService { | |
private Map<String, Board> boards = new LinkedHashMap<>(); | |
private List<Post> posts = new ArrayList<Post>(); | |
private AtomicLong postNumberGenerator = new AtomicLong(0); | |
@PostConstruct | |
public void init() { | |
this.boards.put("notice", new Board("notice")); | |
this.boards.put("random", new Board("random")); | |
this.posts.add(new Post(0, "tester", "no title,", "no content", boards.get("notice"))); | |
} | |
public Board findBoard(String boardname) { | |
Board board = boards.get(boardname); | |
if(Objects.isNull(board)) { | |
throw new BoardNotFoundException(boardname); | |
} | |
return board; | |
} | |
public List<Post> findPosts(String boardname) { | |
Board board = findBoard(boardname); | |
return posts.stream() | |
.filter(post -> board.equals(post.getOwner())) | |
.collect(Collectors.toList()); | |
} | |
public Post writePost(String boardname, String author, String title, String content) { | |
Board board = findBoard(boardname); | |
Post post = new Post(postNumberGenerator.incrementAndGet(), author, title, content, board); | |
posts.add(post); | |
return post; | |
} | |
public Post editPost(long postId, String author, String title, String content) { | |
return posts.stream() | |
.filter(post -> postId == post.getId()) | |
.findAny() | |
.orElseThrow(() -> new PostNotFoundException(postId)) | |
.update(author, title, content); | |
} | |
public Post erasePost(long postId) { | |
Post post = posts.stream() | |
.filter(_post -> postId == _post.getId()) | |
.findAny() | |
.orElseThrow(() -> new PostNotFoundException(postId)); | |
posts.remove(post); | |
return post; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.Locale; | |
import java.util.Map; | |
import java.util.Objects; | |
import javax.servlet.http.HttpServletRequest; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.beans.factory.annotation.Value; | |
import org.springframework.boot.autoconfigure.web.ErrorAttributes; | |
import org.springframework.boot.autoconfigure.web.ErrorController; | |
import org.springframework.context.MessageSource; | |
import org.springframework.http.HttpStatus; | |
import org.springframework.http.ResponseEntity; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
import org.springframework.web.bind.annotation.RestController; | |
import org.springframework.web.context.request.RequestAttributes; | |
import org.springframework.web.context.request.ServletRequestAttributes; | |
@RestController | |
public class ErrorHandler implements ErrorController { | |
private String errorPath; | |
private ErrorAttributes errorAttributes; | |
private MessageSource messageSource; | |
private MessageSourceAccessor messageSourceAccessor; | |
@RequestMapping(value = "${error.path:/error}") | |
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request, Locale locale) { | |
RequestAttributes requestAttributes = new ServletRequestAttributes(request); | |
Map<String, Object> body = errorAttributes.getErrorAttributes(requestAttributes, false); | |
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; | |
if(Objects.nonNull(body.get("status"))) { | |
try { | |
status = HttpStatus.valueOf((Integer) body.get("status")); | |
} catch(Exception ignore) { } | |
} | |
String message = messageSource.getMessage("error." + status, null, null, locale); | |
if(Objects.nonNull(message)) { | |
body.put("message", message); | |
} | |
/* Java 8 | |
messageSourceAccessor.getMessage("error." + status, locale) | |
.ifPresent(message -> body.put("message", message)); | |
*/ | |
return ResponseEntity.status(status).body(body); | |
} | |
@Override | |
public String getErrorPath() { | |
return errorPath; | |
} | |
@Value("${error.path:/error}") | |
public void setErrorPath(String errorPath) { | |
this.errorPath = errorPath; | |
} | |
@Autowired | |
public void setErrorAttributes(ErrorAttributes errorAttributes) { | |
this.errorAttributes = errorAttributes; | |
} | |
@Autowired | |
public void setMessageSource(MessageSource messageSource) { | |
this.messageSource = messageSource; | |
this.messageSourceAccessor = new MessageSourceAccessor(messageSource); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
error.405=요청 URL이 올바르지 않습니다. | |
error.ResourceNotFoundException=요청 자원을 찾을 수 없습니다. | |
error.BoardNotFoundException={0} 게시판을 찾을 수 없습니다. | |
error.PostNotFoundException={0}번 글을 찾을 수 없습니다. | |
error.BindException=전송 데이터가 올바르지 않습니다. | |
NotEmpty.postForm.author=작성자(author)는 필수 입력 값입니다. | |
NotEmpty.postForm.title=제목(title)은 필수 입력 값입니다. | |
NotEmpty.postForm.content=내용(content)은 필수 입력 값입니다. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.Date; | |
public class Post { | |
private Long id; | |
private String author; | |
private String title; | |
private String content; | |
private Date createdAt; | |
private Board owner; | |
public Post(long id, String author, String title, String content, Board owner) { | |
this.id = id; | |
this.author = author; | |
this.title = title; | |
this.content = content; | |
this.createdAt = new Date(); | |
this.owner = owner; | |
} | |
public Long getId() { | |
return id; | |
} | |
public String getAuthor() { | |
return author; | |
} | |
public String getTitle() { | |
return title; | |
} | |
public String getContent() { | |
return content; | |
} | |
public Date getCreatedAt() { | |
return createdAt; | |
} | |
public Board getOwner() { | |
return owner; | |
} | |
public Post update(String author, String title, String content) { | |
this.author = author; | |
this.title = title; | |
this.content = content; | |
return this; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class PostNotFoundException extends RuntimeException { | |
private final long postId; | |
public PostNotFoundException(long postId) { | |
this.postId = postId; | |
} | |
public Object[] getArgs() { | |
return new Object[]{ postId }; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.springframework.http.HttpStatus; | |
public abstract class ResourceNotFoundException extends RuntimeException { | |
public HttpStatus getStatus() { | |
return HttpStatus.NOT_FOUND; | |
} | |
public String getError() { | |
return HttpStatus.NOT_FOUND.getReasonPhrase(); | |
} | |
public String getCode() { | |
return "error.ResourceNotFoundException"; | |
} | |
public Object[] getArgs() { | |
return new Object[0]; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment