diff --git a/README.md b/README.md index 62642bb..15ec987 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # ๐ Spring ์๋ฒ ๋ง๋ค๊ธฐ -ํด๋น ๋ ํฌ์งํ ๋ฆฌ๋ ์๋ฐ ์น ํ๋ก๊ทธ๋๋ฐ Next Step์ ์ฐธ์กฐํด ์งํํฉ๋๋ค. +ํด๋น ๋ ํฌ์งํ ๋ฆฌ๋ [์๋ฐ ์น ํ๋ก๊ทธ๋๋ฐ Next Step](https://m.yes24.com/Goods/Detail/31869154) ๋ฐ [ํด๋น ๋ ํฌ์งํ ๋ฆฌ](https://github.com/next-step)๋ฅผ +์ฐธ์กฐํด ์งํํฉ๋๋ค. > html/css๋ [ํด๋น ๋ ํฌ์งํ ๋ฆฌ](https://github.com/Origogi/DreamCoding-FE-Portfolio-Clone)๋ฅผ ์ฐธ์กฐํ์ต๋๋ค. @@ -9,11 +10,59 @@ +## ๐ป ํ๋ก๊ทธ๋จ ์คํ + +app ๋ชจ๋ application.yml ํ์ผ/์ค์ ์ถ๊ฐ ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ ๊ฐ ๋ฑ๋ก. test ๋๋ ํ ๋ฆฌ์๋ ์ถ๊ฐ. + +````yaml + spring: + datasource: + driver-class-name: ${DRIVER_CLASS_NAME} + url: ${URL} + username: ${USERNAME} + password: ${PASSWORD} +```` + + + + + + +๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ์์ฑ. app ๋ชจ๋์ resource ํจํค์ง ์ฐธ์กฐ. + +```sql +CREATE TABLE user +( + id BIGINT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 'PK', + username VARCHAR(40) NOT NULL COMMENT '์ฌ์ฉ์ ์ด๋ฆ', + password VARCHAR(255) NOT NULL COMMENT 'ํจ์ค์๋', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '์์ฑ์ผ', + last_modified_at TIMESTAMP NULL DEFAULT NULL COMMENT '์ต์ข ์์ ์ผ', + deleted VARCHAR(10) NOT NULL COMMENT '์ญ์ ์ ๋ฌด' +) engine 'InnoDB'; +``` + + + + + + +๋น๋ ํ ํ๋ก๊ทธ๋จ์ ์คํํฉ๋๋ค. + +```shell +./gradlew build +./gradlew bootJar +``` + + + + + + ## ๐ ๊ณตํต ์๊ตฌ์ฌํญ -1. ์ ์ฒด ๋ฏธ์ ์ 7๋จ๊ณ๋ก ๋๋์ด์ ธ ์์ผ๋ฉฐ, ๊ฐ Step์๋ `ํ์/์ ํ ๊ตฌํ ์ฌํญ`, `ํ์ต ๋ชฉํ`๊ฐ ์ฃผ์ด์ง๋๋ค. +1. ์ ์ฒด ๋ฏธ์ ์ 4๋จ๊ณ๋ก ๋๋์ด์ ธ ์์ผ๋ฉฐ, ๊ฐ Step์๋ `ํ์/์ ํ ๊ตฌํ ์ฌํญ`, `ํ์ต ๋ชฉํ`๊ฐ ์ฃผ์ด์ง๋๋ค. - ๋ค์ ๋จ๊ณ๋ก ๋์ด๊ฐ๊ธฐ ์ํด์๋ ์ต์ 1๋ช ์ด์์ Approve๊ฐ ํ์ํฉ๋๋ค. - - ํ์ ์คํ ์ 5๋จ๊ณ ๊น์ง์ด๋ฉฐ, ๋๋จธ์ง ๋จ๊ณ๋ ์์จ์ ์ผ๋ก ์งํํฉ๋๋ค. - ํ์ ๊ตฌํ์ฌํญ์ ๋ฐ๋์ ๊ตฌํํด์ผ ํ๋ฉฐ, ์ ํ ๊ตฌํ ์ฌํญ์ ๊ตฌํํ์ง ์์๋ ๋ฉ๋๋ค. - ์ ํ ๊ตฌํ์ฌํญ์๋ [์ ํ] ์ด ๋ช ์ ๋ผ ์์ผ๋ฉฐ, ์๋ค๋ฉด ํ์ ๊ตฌํ์ฌํญ์ ๋๋ค. - ํ์ฌ ์งํ์ค์ธ Step์ด ์๋ฃ๋์ง ์์ผ๋ฉด, ๋ค์ ๋จ๊ณ๋ก ๋์ด๊ฐ ์ ์์ต๋๋ค. @@ -27,10 +76,8 @@ - - -## Step1. ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ค. +## [Step1] ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ค. ๋คํธ์ํฌ๋ก ๋ถํฐ ์ ์ก๋ ๋ฐ์ดํฐ๋ฅผ ํ์ฑํด ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ค. @@ -39,7 +86,6 @@ - [x] ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ์ ํ๋ฆฌ์ผ์ด์ ๋ด๋ถ ์ธ๋ฉ๋ชจ๋ฆฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํ๋ค. - [x] ์ด๋ค ์ ๋ณด๋ฅผ ์ ์ฅํ ์ง๋ ์์ ๋กญ๊ฒ ์ ์ํ๋ค. - ### ํ์ต ๋ชฉํ @@ -50,10 +96,8 @@ - - -## Step2. ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ค. +## [Step2] ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ค. ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ค. @@ -65,8 +109,6 @@ - [x] ๊ฐ์ธ ์ ๋ณด ์์ธ ์กฐํ ๊ธฐ๋ฅ์ ๊ฐ๋ฐํ๋ค. - - ### ํ์ต ๋ชฉํ @@ -80,10 +122,8 @@ - - -## Step3. ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๊ต์ฒดํ๋ค. +## [Step3] ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๊ต์ฒดํ๋ค. ์ ํ๋ฆฌ์ผ์ด์ ๋ด๋ถ์ ์ ์ฅํ๋ ๋ฐ์ดํฐ๋ฅผ ์ธ๋ถ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๋ค. @@ -91,12 +131,30 @@ - RDB, Redis ๋ฑ 2. JDBC ํ ํ๋ฆฟ์ ๊ตฌํํ๋ค. - ### ํ์ต ๋ชฉํ -1. ์ถ์ํ์ ๋ํด ์ดํดํ๋ค. +1. ์ถ์ํ์ ๋ํด ํ์ตํ๋ค. 2. ๋ฐ์ดํฐ๋ฒ ์ด์ค ํต์ ๊ณผ์ ์ ๋ํด ์ดํดํ๋ค. 3. ๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํน์ง์ ๋ํด ์ดํดํ๋ค. 4. ํธ๋์ญ์ ์ ๋ํด ํ์ตํ๋ค. + + + + + + +## [Step4] ๋ฐ์ดํฐ ์ ์ก ๋ฐฉ์์ ์ผ๋ถ ๋ณ๊ฒฝํ๋ค. + +๋งค ๋ฒ ์ ์ ๋ฆฌ์์ค๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฒ์ ๋นํจ์จ์ ์ด๊ธฐ ๋๋ฌธ์, ํ ํ์ด์ง์์ ์ผ๋ถ ๋ฐ์ดํฐ๋ง ๋ณ๊ฒฝํ ์ ์๋๋ก, ๋ฐ์ดํฐ ์ ์ก ๋ฐฉ์์ ๋ณ๊ฒฝํ๋ค. + +1. ๋ชจ๋ API์ ์ ์ฉํ ํ์ ์์ผ๋ฉฐ, ๊ฐ์ธ์ ๋ณด ์์ ๋ง ์ ์ฉํ๋ค. +2. [์ ํ] ์ฝ๋๋ฅผ ๋ฆฌํฉํ ๋งํ๋ค. + + + +### ํ์ต ๋ชฉํ + +1. ๊ฐ ๋ฐ์ดํฐ ์ ์ก ๋ฐฉ์์ ๋ํด ํ์ตํ๋ค. +2. ์ ์ ๋ฆฌ์์ค๋ฅผ ๊ฐ์ ธ์ค๋ ๋น์ฉ์ ์ต์ ํ ํ๋ ๋ฐฉ๋ฒ์ ๋ํด ํ์ตํ๋ค. diff --git a/app/build.gradle b/app/build.gradle index 359f72b..5d358e3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -92,5 +92,5 @@ task downloadYml { } tasks.named("downloadYml") { - dependsOn downloadYml + dependsOn compileJava } diff --git a/app/src/main/java/project/server/app/common/configuration/WebConfiguration.java b/app/src/main/java/project/server/app/common/configuration/WebConfiguration.java new file mode 100644 index 0000000..9a1c4b6 --- /dev/null +++ b/app/src/main/java/project/server/app/common/configuration/WebConfiguration.java @@ -0,0 +1,33 @@ +package project.server.app.common.configuration; + +import project.server.app.common.configuration.interceptor.SessionCheckHandlerInterceptor; +import project.server.app.core.web.user.application.UserLoginUseCase; +import project.server.app.core.web.user.presentation.validator.UserValidator; +import project.server.mvc.springframework.annotation.Component; +import static project.server.mvc.springframework.context.ApplicationContext.getBean; +import static project.server.mvc.springframework.context.ApplicationContext.register; +import project.server.mvc.springframework.web.InterceptorRegistry; +import project.server.mvc.springframework.web.WebMvcConfigurer; + +@Component +public class WebConfiguration implements WebMvcConfigurer { + + private final UserValidator validator; + private final UserLoginUseCase loginUseCase; + + public WebConfiguration( + UserValidator validator, + UserLoginUseCase loginUseCase + ) { + this.validator = validator; + this.loginUseCase = loginUseCase; + register(new InterceptorRegistry()); + addInterceptors(getBean(InterceptorRegistry.class)); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new SessionCheckHandlerInterceptor(validator, loginUseCase)) + .addPathPatterns("/my-info.html", "/my-info", "/api/users"); + } +} diff --git a/app/src/main/java/project/server/app/common/configuration/interceptor/SessionCheckHandlerInterceptor.java b/app/src/main/java/project/server/app/common/configuration/interceptor/SessionCheckHandlerInterceptor.java new file mode 100644 index 0000000..2226cf3 --- /dev/null +++ b/app/src/main/java/project/server/app/common/configuration/interceptor/SessionCheckHandlerInterceptor.java @@ -0,0 +1,64 @@ +package project.server.app.common.configuration.interceptor; + +import lombok.extern.slf4j.Slf4j; +import project.server.app.common.login.LoginUser; +import project.server.app.common.login.Session; +import static project.server.app.common.utils.HeaderUtils.getSessionId; +import project.server.app.core.web.user.application.UserLoginUseCase; +import project.server.app.core.web.user.presentation.validator.UserValidator; +import project.server.mvc.servlet.HttpServletRequest; +import project.server.mvc.servlet.HttpServletResponse; +import project.server.mvc.springframework.handler.HandlerInterceptor; +import project.server.mvc.springframework.web.servlet.ModelAndView; + +@Slf4j +public class SessionCheckHandlerInterceptor implements HandlerInterceptor { + + private final UserValidator validator; + private final UserLoginUseCase loginUseCase; + + public SessionCheckHandlerInterceptor( + UserValidator validator, + UserLoginUseCase loginUseCase + ) { + this.validator = validator; + this.loginUseCase = loginUseCase; + } + + @Override + public boolean preHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler + ) { + Long sessionId = getSessionId(request.getCookies()); + validator.validateSessionId(sessionId, response); + + Session findSession = loginUseCase.findSessionById(sessionId); + log.info("Session:{}", findSession); + validator.validateSession(findSession, response); + + LoginUser loginUser = new LoginUser(findSession); + request.setAttribute("loginUser", loginUser); + return false; + } + + @Override + public void postHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler, + ModelAndView modelAndView + ) { + HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); + } + + @Override + public void afterCompletion( + HttpServletRequest request, + HttpServletResponse response, + Object handler + ) { + HandlerInterceptor.super.afterCompletion(request, response, handler); + } +} diff --git a/app/src/main/java/project/server/app/common/utils/HeaderUtils.java b/app/src/main/java/project/server/app/common/utils/HeaderUtils.java index 779e571..93fcef9 100644 --- a/app/src/main/java/project/server/app/common/utils/HeaderUtils.java +++ b/app/src/main/java/project/server/app/common/utils/HeaderUtils.java @@ -1,8 +1,6 @@ package project.server.app.common.utils; -import java.util.Map; -import project.server.mvc.servlet.http.Cookie; -import project.server.mvc.servlet.http.Cookies; +import project.server.mvc.servlet.http.*; public final class HeaderUtils { @@ -13,8 +11,7 @@ private HeaderUtils() { } public static Long getSessionId(Cookies cookies) { - Map cookiesMap = cookies.getCookiesMap(); - Cookie findCookie = cookiesMap.get(SESSION_ID); + Cookie findCookie = cookies.get(SESSION_ID); if (findCookie != null) { return extractSessionId(findCookie); } diff --git a/app/src/main/java/project/server/app/core/domain/user/UserRepository.java b/app/src/main/java/project/server/app/core/domain/user/UserRepository.java index 600ba44..0ac4bc6 100644 --- a/app/src/main/java/project/server/app/core/domain/user/UserRepository.java +++ b/app/src/main/java/project/server/app/core/domain/user/UserRepository.java @@ -14,6 +14,8 @@ public interface UserRepository { Optional findByUsernameAndPassword(String username, String password); + void update(Long id, String password); + void delete(User user); List findAll(); diff --git a/app/src/main/java/project/server/app/core/web/user/application/UserUpdateUseCase.java b/app/src/main/java/project/server/app/core/web/user/application/UserUpdateUseCase.java new file mode 100644 index 0000000..6d1e3d7 --- /dev/null +++ b/app/src/main/java/project/server/app/core/web/user/application/UserUpdateUseCase.java @@ -0,0 +1,7 @@ +package project.server.app.core.web.user.application; + +import project.server.app.common.login.LoginUser; + +public interface UserUpdateUseCase { + void update(LoginUser loginUser, String password); +} diff --git a/app/src/main/java/project/server/app/core/web/user/application/service/UserService.java b/app/src/main/java/project/server/app/core/web/user/application/service/UserService.java index 11c4f63..731aed9 100644 --- a/app/src/main/java/project/server/app/core/web/user/application/service/UserService.java +++ b/app/src/main/java/project/server/app/core/web/user/application/service/UserService.java @@ -7,12 +7,13 @@ import project.server.app.core.web.user.application.UserDeleteUseCase; import project.server.app.core.web.user.application.UserSaveUseCase; import project.server.app.core.web.user.application.UserSearchUseCase; +import project.server.app.core.web.user.application.UserUpdateUseCase; import project.server.app.core.web.user.exception.DuplicatedUsernameException; import project.server.app.core.web.user.exception.UserNotFoundException; import project.server.mvc.springframework.annotation.Service; @Service -public class UserService implements UserSaveUseCase, UserSearchUseCase, UserDeleteUseCase { +public class UserService implements UserSaveUseCase, UserSearchUseCase, UserUpdateUseCase, UserDeleteUseCase { private final UserRepository userRepository; @@ -43,6 +44,16 @@ public User findById(Long userId) { return findUser; } + @Override + public void update( + LoginUser loginUser, + String password + ) { + User findUser = userRepository.findById(loginUser.getUserId()) + .orElseThrow(UserNotFoundException::new); + userRepository.update(findUser.getId(), password); + } + @Override public void delete(LoginUser loginUser) { User findUser = userRepository.findById(loginUser.getUserId()) diff --git a/app/src/main/java/project/server/app/core/web/user/application/service/UserServiceProxy.java b/app/src/main/java/project/server/app/core/web/user/application/service/UserServiceProxy.java index d25301c..fc2cf7f 100644 --- a/app/src/main/java/project/server/app/core/web/user/application/service/UserServiceProxy.java +++ b/app/src/main/java/project/server/app/core/web/user/application/service/UserServiceProxy.java @@ -10,6 +10,7 @@ import project.server.app.core.web.user.application.UserDeleteUseCase; import project.server.app.core.web.user.application.UserSaveUseCase; import project.server.app.core.web.user.application.UserSearchUseCase; +import project.server.app.core.web.user.application.UserUpdateUseCase; import project.server.jdbc.core.exception.DataAccessException; import static project.server.jdbc.core.transaction.DefaultTransactionDefinition.createTransactionDefinition; import project.server.jdbc.core.transaction.PlatformTransactionManager; @@ -18,7 +19,7 @@ @Slf4j @Component -public class UserServiceProxy implements UserSaveUseCase, UserSearchUseCase, UserDeleteUseCase { +public class UserServiceProxy implements UserSaveUseCase, UserSearchUseCase, UserUpdateUseCase, UserDeleteUseCase { private final PlatformTransactionManager txManager; private final UserService target; @@ -70,6 +71,24 @@ public User findById(Long userId) { } } + @Override + public void update( + LoginUser loginUser, + String password + ) { + TransactionStatus txStatus = getTransactionStatus(true); + log.debug("txStatus:[{}]", txStatus.getTransaction()); + try { + target.update(loginUser, password); + txManager.commit(txStatus); + log.debug("Transaction finished."); + } catch (BusinessException | DataAccessException exception) { + txManager.rollback(txStatus); + log.error("{}", exception.getMessage()); + throw exception; + } + } + @Override public void delete(LoginUser loginUser) { TransactionStatus txStatus = getTransactionStatus(false); diff --git a/app/src/main/java/project/server/app/core/web/user/persistence/UserPersistenceRepository.java b/app/src/main/java/project/server/app/core/web/user/persistence/UserPersistenceRepository.java index 3f73e41..4397c94 100644 --- a/app/src/main/java/project/server/app/core/web/user/persistence/UserPersistenceRepository.java +++ b/app/src/main/java/project/server/app/core/web/user/persistence/UserPersistenceRepository.java @@ -13,11 +13,12 @@ import project.server.app.core.domain.user.User; import project.server.app.core.domain.user.UserRepository; import project.server.jdbc.core.exception.DataAccessException; +import project.server.jdbc.core.jdbc.JdbcHelper; import static project.server.jdbc.core.jdbc.JdbcHelper.insert; import static project.server.jdbc.core.jdbc.JdbcHelper.selectAll; import static project.server.jdbc.core.jdbc.JdbcHelper.selectBy; import static project.server.jdbc.core.jdbc.JdbcHelper.truncate; -import static project.server.jdbc.core.jdbc.JdbcHelper.update; +import static project.server.jdbc.core.jdbc.JdbcHelper.updatePassword; import project.server.jdbc.core.jdbc.JdbcTemplate; import project.server.jdbc.core.jdbc.RowMapper; import project.server.mvc.springframework.annotation.Repository; @@ -88,9 +89,23 @@ public Optional findByUsernameAndPassword( ); } + @Override + public void update( + Long id, + String password + ) { + String sql = updatePassword(User.class, "password"); + jdbcTemplate.queryForObject(sql, pstmt -> { + pstmt.setString(1, password); + pstmt.setLong(2, id); + pstmt.executeUpdate(); + return null; + }); + } + @Override public void delete(User user) { - String sql = update(User.class, "deleted"); + String sql = JdbcHelper.update(User.class, "deleted"); jdbcTemplate.queryForObject(sql, pstmt -> { pstmt.setLong(1, user.getId()); pstmt.executeUpdate(); diff --git a/app/src/main/java/project/server/app/core/web/user/persistence/UserRowMapper.java b/app/src/main/java/project/server/app/core/web/user/persistence/UserRowMapper.java index 83b6361..4364f8a 100644 --- a/app/src/main/java/project/server/app/core/web/user/persistence/UserRowMapper.java +++ b/app/src/main/java/project/server/app/core/web/user/persistence/UserRowMapper.java @@ -24,8 +24,8 @@ public User mapRow(ResultSet rs) throws SQLException { String username = rs.getString(USERNAME); String password = rs.getString(PASSWORD); LocalDateTime createdAt = rs.getTimestamp(CREATED_AT).toLocalDateTime(); - LocalDateTime lastModifiedAt = rs.getTimestamp(LAST_MODIFIED_AT) != null ? - rs.getTimestamp(LAST_MODIFIED_AT).toLocalDateTime() : null; + LocalDateTime lastModifiedAt = rs.getTimestamp(LAST_MODIFIED_AT) != null + ? rs.getTimestamp(LAST_MODIFIED_AT).toLocalDateTime() : null; Deleted deleted = Deleted.valueOf(rs.getString(DELETED)); return new User(id, username, password, createdAt, lastModifiedAt, deleted); } diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/HomeController.java b/app/src/main/java/project/server/app/core/web/user/presentation/HomeController.java index a70b8d9..eb2d3fa 100644 --- a/app/src/main/java/project/server/app/core/web/user/presentation/HomeController.java +++ b/app/src/main/java/project/server/app/core/web/user/presentation/HomeController.java @@ -1,17 +1,17 @@ package project.server.app.core.web.user.presentation; +import project.server.mvc.servlet.HttpServletRequest; +import project.server.mvc.servlet.HttpServletResponse; import project.server.mvc.springframework.annotation.Controller; import project.server.mvc.springframework.annotation.GetMapping; import project.server.mvc.springframework.web.servlet.Handler; -import project.server.mvc.servlet.HttpServletRequest; -import project.server.mvc.servlet.HttpServletResponse; import project.server.mvc.springframework.web.servlet.ModelAndView; @Controller public class HomeController implements Handler { @Override - @GetMapping("/") + @GetMapping(path = "/") public ModelAndView process( HttpServletRequest request, HttpServletResponse response diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/LoginController.java b/app/src/main/java/project/server/app/core/web/user/presentation/LoginController.java index 7b5f02e..60cbd3f 100644 --- a/app/src/main/java/project/server/app/core/web/user/presentation/LoginController.java +++ b/app/src/main/java/project/server/app/core/web/user/presentation/LoginController.java @@ -9,6 +9,7 @@ import project.server.mvc.servlet.http.Cookie; import static project.server.mvc.servlet.http.HttpStatus.MOVE_PERMANENTLY; import project.server.mvc.springframework.annotation.Controller; +import project.server.mvc.springframework.annotation.PostMapping; import project.server.mvc.springframework.web.servlet.Handler; import project.server.mvc.springframework.web.servlet.ModelAndView; @@ -30,13 +31,13 @@ public LoginController( } @Override + @PostMapping(path = "/sign-in") public ModelAndView process( HttpServletRequest request, HttpServletResponse response ) { - String username = request.getAttribute("username"); - String password = request.getAttribute("password"); - log.info("username: {}, password: {}", username, password); + String username = (String) request.getAttribute("username"); + String password = (String) request.getAttribute("password"); validator.validateLoginInfo(username, password); Session session = userLoginUseCase.login(username, password); diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/SignUpController.java b/app/src/main/java/project/server/app/core/web/user/presentation/SignUpController.java index efa1955..1ae5b51 100644 --- a/app/src/main/java/project/server/app/core/web/user/presentation/SignUpController.java +++ b/app/src/main/java/project/server/app/core/web/user/presentation/SignUpController.java @@ -7,6 +7,7 @@ import project.server.mvc.servlet.HttpServletResponse; import static project.server.mvc.servlet.http.HttpStatus.OK; import project.server.mvc.springframework.annotation.Controller; +import project.server.mvc.springframework.annotation.PostMapping; import project.server.mvc.springframework.annotation.RequestMapping; import project.server.mvc.springframework.web.servlet.Handler; import project.server.mvc.springframework.web.servlet.ModelAndView; @@ -28,12 +29,13 @@ public SignUpController( } @Override + @PostMapping(path = "/sign-up") public ModelAndView process( HttpServletRequest request, HttpServletResponse response ) { - String username = request.getAttribute("username"); - String password = request.getAttribute("password"); + String username = (String) request.getAttribute("username"); + String password = (String) request.getAttribute("password"); log.info("username: {}, password: {}", username, password); validator.validateLoginInfo(username, password); diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/UserDeleteController.java b/app/src/main/java/project/server/app/core/web/user/presentation/UserDeleteController.java index d82de8c..6f02dbc 100644 --- a/app/src/main/java/project/server/app/core/web/user/presentation/UserDeleteController.java +++ b/app/src/main/java/project/server/app/core/web/user/presentation/UserDeleteController.java @@ -11,6 +11,7 @@ import project.server.mvc.servlet.HttpServletResponse; import static project.server.mvc.servlet.http.HttpStatus.NO_CONTENT; import project.server.mvc.springframework.annotation.Controller; +import project.server.mvc.springframework.annotation.DeleteMapping; import project.server.mvc.springframework.web.servlet.Handler; import project.server.mvc.springframework.web.servlet.ModelAndView; diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/UserInfoSearchController.java b/app/src/main/java/project/server/app/core/web/user/presentation/UserInfoSearchController.java index 593022d..e9bbae2 100644 --- a/app/src/main/java/project/server/app/core/web/user/presentation/UserInfoSearchController.java +++ b/app/src/main/java/project/server/app/core/web/user/presentation/UserInfoSearchController.java @@ -2,12 +2,8 @@ import lombok.extern.slf4j.Slf4j; import project.server.app.common.login.LoginUser; -import project.server.app.common.login.Session; -import static project.server.app.common.utils.HeaderUtils.getSessionId; import project.server.app.core.domain.user.User; -import project.server.app.core.web.user.application.UserLoginUseCase; import project.server.app.core.web.user.application.UserSearchUseCase; -import project.server.app.core.web.user.presentation.validator.UserValidator; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; import static project.server.mvc.servlet.http.HttpStatus.OK; @@ -23,34 +19,19 @@ @RequestMapping("/users") public class UserInfoSearchController implements Handler { - private final UserValidator validator; - private final UserLoginUseCase loginUseCase; private final UserSearchUseCase userSearchUseCase; - public UserInfoSearchController( - UserValidator validator, - UserLoginUseCase loginUseCase, - UserSearchUseCase userSearchUseCase - ) { - this.validator = validator; - this.loginUseCase = loginUseCase; + public UserInfoSearchController(UserSearchUseCase userSearchUseCase) { this.userSearchUseCase = userSearchUseCase; } @Override - @GetMapping(path = "/users/{userId}") + @GetMapping(path = "/my-info") public ModelAndView process( HttpServletRequest request, HttpServletResponse response ) { - Long sessionId = getSessionId(request.getCookies()); - validator.validateSessionId(sessionId, response); - - Session findSession = loginUseCase.findSessionById(sessionId); - validator.validateSession(findSession, response); - - log.info("Session:{}", findSession); - LoginUser loginUser = new LoginUser(findSession); + LoginUser loginUser = (LoginUser) request.getAttribute("loginUser"); User findUser = userSearchUseCase.findById(loginUser.getUserId()); ModelMap modelMap = createModelMap(findUser); diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/UserInfoUpdateController.java b/app/src/main/java/project/server/app/core/web/user/presentation/UserInfoUpdateController.java new file mode 100644 index 0000000..5505d58 --- /dev/null +++ b/app/src/main/java/project/server/app/core/web/user/presentation/UserInfoUpdateController.java @@ -0,0 +1,46 @@ +package project.server.app.core.web.user.presentation; + +import lombok.extern.slf4j.Slf4j; +import project.server.app.common.login.LoginUser; +import project.server.app.core.web.user.application.UserLoginUseCase; +import project.server.app.core.web.user.application.UserUpdateUseCase; +import project.server.app.core.web.user.presentation.validator.UserValidator; +import project.server.mvc.servlet.HttpServletRequest; +import project.server.mvc.servlet.HttpServletResponse; +import static project.server.mvc.servlet.http.HttpStatus.OK; +import project.server.mvc.springframework.annotation.PutMapping; +import project.server.mvc.springframework.annotation.RestController; +import project.server.mvc.springframework.web.servlet.Handler; +import project.server.mvc.springframework.web.servlet.ModelAndView; + +@Slf4j +@RestController +public class UserInfoUpdateController implements Handler { + + private final UserValidator validator; + private final UserLoginUseCase loginUseCase; + private final UserUpdateUseCase userUpdateUseCase; + + public UserInfoUpdateController( + UserValidator validator, + UserLoginUseCase loginUseCase, + UserUpdateUseCase userUpdateUseCase + ) { + this.validator = validator; + this.loginUseCase = loginUseCase; + this.userUpdateUseCase = userUpdateUseCase; + } + + @Override + @PutMapping(path = "/api/users") + public ModelAndView process( + HttpServletRequest request, + HttpServletResponse response + ) { + LoginUser loginUser = (LoginUser) request.getAttribute("loginUser"); + userUpdateUseCase.update(loginUser, (String) request.getAttribute("password")); + + response.setStatus(OK); + return null; + } +} diff --git a/app/src/main/resources/schema.sql b/app/src/main/resources/schema.sql new file mode 100644 index 0000000..ccf0c3a --- /dev/null +++ b/app/src/main/resources/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE user +( + id BIGINT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 'PK', + username VARCHAR(40) NOT NULL COMMENT '์ฌ์ฉ์ ์ด๋ฆ', + password VARCHAR(255) NOT NULL COMMENT 'ํจ์ค์๋', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '์์ฑ์ผ', + last_modified_at TIMESTAMP NULL DEFAULT NULL COMMENT '์ต์ข ์์ ์ผ', + deleted VARCHAR(10) NOT NULL COMMENT '์ญ์ ์ ๋ฌด' +) engine 'InnoDB'; diff --git a/app/src/main/resources/static/index.html b/app/src/main/resources/static/index.html index 75ffbb3..d4ff44a 100644 --- a/app/src/main/resources/static/index.html +++ b/app/src/main/resources/static/index.html @@ -136,10 +136,16 @@ Infra window.location.href = signInMenu.getAttribute('href'); } else { console.error('Server responded with non-OK status'); + localStorage.clear(); + document.cookie = 'sessionId=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'; + window.location.href = "http://localhost:8080/"; } }) .catch((error) => { console.error('Error:', error); + localStorage.clear(); + document.cookie = 'sessionId=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'; + window.location.href = "http://localhost:8080/"; }); }); } diff --git a/app/src/main/resources/static/my-info.html b/app/src/main/resources/static/my-info.html index ff75362..c6f96ee 100644 --- a/app/src/main/resources/static/my-info.html +++ b/app/src/main/resources/static/my-info.html @@ -1,12 +1,10 @@ - ํ์ ์์ธ์ ๋ณด - Bootstrap - - @@ -40,78 +37,57 @@ ํ์ ์์ธ์ ๋ณด ID - + ID๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์. ํจ์ค์๋ - + ๋น๋ฐ๋ฒํธ๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์. - - ์ด๋ฉ์ผ - - - ์ด๋ฉ์ผ์ ์ ๋ ฅํด์ฃผ์ธ์. - - - - ์ฃผ์ (ํ์ ์๋) - - - ์ฃผ์๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์. - - - - ์์ธ์ฃผ์ (ํ์ ์๋) - - - - - - ๊ฐ์ธ์ ๋ณด ์์ง ๋ฐ ์ด์ฉ์ ๋์ํฉ๋๋ค. - - ์์ - - - diff --git a/app/src/main/resources/static/sign-in.html b/app/src/main/resources/static/sign-in.html index dfd1138..b17f777 100644 --- a/app/src/main/resources/static/sign-in.html +++ b/app/src/main/resources/static/sign-in.html @@ -98,12 +98,12 @@ If you're first-com document.querySelector('.sign-up-container form').addEventListener('submit', function (event) { event.preventDefault(); var xhr = new XMLHttpRequest(); - xhr.open("POST", "http://localhost:8086/sign-up", true); + xhr.open("POST", "http://localhost:8080/sign-up", true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.onload = function () { if (xhr.status === 200 || xhr.status === 301) { alert("ํ์๊ฐ์ ์ด ์๋ฃ๋์์ต๋๋ค."); - window.location.href = "http://localhost:8086/sign-in.html"; + window.location.href = "http://localhost:8080/sign-in.html"; } else { alert("ํ์๊ฐ์ ์ ์คํจํ์ต๋๋ค."); } @@ -121,17 +121,12 @@ If you're first-com event.preventDefault(); var xhr = new XMLHttpRequest(); - xhr.open("POST", "http://localhost:8086/sign-in", true); + xhr.open("POST", "http://localhost:8080/sign-in", true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.onload = function () { if (xhr.status === 200 || xhr.status === 301) { - // var logined = xhr.getResponseHeader("logined"); - // if (logined) { localStorage.setItem("logined", true); - window.location.href = "http://localhost:8086/"; - // } else { - // alert("๋ก๊ทธ์ธ์ ์คํจํ์ต๋๋ค."); - // } + window.location.href = "http://localhost:8080/"; } else { alert("Error: " + xhr.status); } diff --git a/app/src/test/java/project/server/app/test/integrationtest/IntegrationTestBase.java b/app/src/test/java/project/server/app/test/integrationtest/IntegrationTestBase.java index ec7c126..1b19f2d 100644 --- a/app/src/test/java/project/server/app/test/integrationtest/IntegrationTestBase.java +++ b/app/src/test/java/project/server/app/test/integrationtest/IntegrationTestBase.java @@ -1,6 +1,7 @@ package project.server.app.test.integrationtest; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import project.server.app.core.domain.user.UserRepository; import project.server.app.core.web.user.persistence.UserPersistenceRepository; import project.server.mvc.springframework.context.ApplicationContext; @@ -21,7 +22,7 @@ protected IntegrationTestBase() { } } - @AfterEach + @BeforeEach void setUp() { userRepository.clear(); } diff --git a/app/src/test/java/project/server/app/test/unittest/user/UserValidatorUnitTest.java b/app/src/test/java/project/server/app/test/unittest/user/UserValidatorUnitTest.java index 9559bac..62c19e7 100644 --- a/app/src/test/java/project/server/app/test/unittest/user/UserValidatorUnitTest.java +++ b/app/src/test/java/project/server/app/test/unittest/user/UserValidatorUnitTest.java @@ -72,4 +72,13 @@ void sessionIdValidateTest() { HttpServletResponse response = new Response(); assertDoesNotThrow(() -> validator.validateSessionId(1L, response)); } + + @Test + @DisplayName("์ธ์ ์ด ์กด์ฌํ์ง ์์ผ๋ฉด UnAuthorizedException์ด ๋ฐ์ํ๋ค.") + void sessionNullTest() { + HttpServletResponse response = new Response(); + assertThatThrownBy(() -> validator.validateSession(null, response)) + .isInstanceOf(RuntimeException.class) + .isExactlyInstanceOf(UnAuthorizedException.class); + } } diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcHelper.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcHelper.java index a259bfd..c9ed6ad 100644 --- a/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcHelper.java +++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcHelper.java @@ -46,6 +46,17 @@ public static String update( + " = 'TRUE' WHERE id = ?"; } + public static String updatePassword( + Class> clazz, + String fieldName + ) { + return "UPDATE " + + convertCamelToSnake(clazz.getSimpleName()) + + " SET " + + convertCamelToSnake(fieldName) + + " = ? WHERE id = ?"; + } + public static String selectAll(Class> clazz) { return "SELECT * FROM " + convertCamelToSnake(clazz.getSimpleName()); diff --git a/mvc/src/main/java/project/server/mvc/AbstractJsonHttpMessageConverter.java b/mvc/src/main/java/project/server/mvc/AbstractJsonHttpMessageConverter.java new file mode 100644 index 0000000..a59a274 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/AbstractJsonHttpMessageConverter.java @@ -0,0 +1,4 @@ +package project.server.mvc; + +public abstract class AbstractJsonHttpMessageConverter { +} diff --git a/mvc/src/main/java/project/server/mvc/Application.java b/mvc/src/main/java/project/server/mvc/Application.java index cca29a4..e5f42bc 100644 --- a/mvc/src/main/java/project/server/mvc/Application.java +++ b/mvc/src/main/java/project/server/mvc/Application.java @@ -21,8 +21,7 @@ public Application( } private void initContext(String packages) throws Exception { - ApplicationContext context; - context = new ApplicationContext(packages); + ApplicationContext context = new ApplicationContext(packages); ApplicationContextProvider provider = new ApplicationContextProvider(); provider.setApplicationContext(context); } diff --git a/mvc/src/main/java/project/server/mvc/HttpMessageConverter.java b/mvc/src/main/java/project/server/mvc/HttpMessageConverter.java new file mode 100644 index 0000000..5ee203b --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/HttpMessageConverter.java @@ -0,0 +1,12 @@ +package project.server.mvc; + +import java.io.IOException; +import project.server.mvc.servlet.HttpServletRequest; +import project.server.mvc.servlet.HttpServletResponse; +import project.server.mvc.servlet.http.ContentType; + +public interface HttpMessageConverter { + boolean canWrite(ContentType contentType); + + void write(HttpServletRequest request, HttpServletResponse response) throws IOException; +} diff --git a/mvc/src/main/java/project/server/mvc/JsonbHttpMessageConverter.java b/mvc/src/main/java/project/server/mvc/JsonbHttpMessageConverter.java new file mode 100644 index 0000000..ca5d7cb --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/JsonbHttpMessageConverter.java @@ -0,0 +1,4 @@ +package project.server.mvc; + +public class JsonbHttpMessageConverter extends AbstractJsonHttpMessageConverter { +} diff --git a/mvc/src/main/java/project/server/mvc/servlet/GenericServlet.java b/mvc/src/main/java/project/server/mvc/servlet/GenericServlet.java index 07c8ba8..1646229 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/GenericServlet.java +++ b/mvc/src/main/java/project/server/mvc/servlet/GenericServlet.java @@ -1,9 +1,20 @@ package project.server.mvc.servlet; +import static project.server.mvc.springframework.context.ApplicationContext.register; +import project.server.mvc.springframework.web.method.HandlerMethod; +import project.server.mvc.springframework.web.servlet.handler.HandlerMappingInitializer; +import project.server.mvc.springframework.web.servlet.mvc.method.RequestMappingHandlerMapping; +import project.server.mvc.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import project.server.mvc.springframework.web.servlet.resource.ResourceHttpRequestHandler; + public abstract class GenericServlet implements Servlet { @Override public void init() { + register(new HandlerMethod(new ResourceHttpRequestHandler())); + register(new HandlerMappingInitializer()); + register(new RequestMappingHandlerMapping()); + register(new RequestMappingHandlerAdapter()); } @Override diff --git a/mvc/src/main/java/project/server/mvc/servlet/HttpServlet.java b/mvc/src/main/java/project/server/mvc/servlet/HttpServlet.java index 18e318a..8e7eae7 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/HttpServlet.java +++ b/mvc/src/main/java/project/server/mvc/servlet/HttpServlet.java @@ -14,7 +14,6 @@ public void service( httpServletRequest = (HttpServletRequest) request; httpServletResponse = (HttpServletResponse) response; - service(httpServletRequest, httpServletResponse); } @@ -31,6 +30,9 @@ protected void service( doPost(request, response); return; } + if (method.isPut()) { + doPut(request, response); + } } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws Exception { @@ -38,4 +40,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t protected void doPost(HttpServletRequest request, HttpServletResponse response) throws Exception { } + + protected void doPut(HttpServletRequest request, HttpServletResponse response) throws Exception { + } } diff --git a/mvc/src/main/java/project/server/mvc/servlet/HttpServletRequest.java b/mvc/src/main/java/project/server/mvc/servlet/HttpServletRequest.java index 23824df..b4b3043 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/HttpServletRequest.java +++ b/mvc/src/main/java/project/server/mvc/servlet/HttpServletRequest.java @@ -1,5 +1,6 @@ package project.server.mvc.servlet; +import project.server.mvc.servlet.http.ContentType; import project.server.mvc.servlet.http.Cookies; import project.server.mvc.servlet.http.HttpMethod; import project.server.mvc.servlet.http.RequestBody; @@ -11,7 +12,10 @@ public interface HttpServletRequest extends ServletRequest { String getHost(); @Override - String getContentType(); + ContentType getContentType(); + + @Override + String getContentTypeAsString(); RequestLine getRequestLine(); @@ -21,9 +25,13 @@ public interface HttpServletRequest extends ServletRequest { RequestBody getRequestBody(); - String getAttribute(String key); + Object getAttribute(String key); Cookies getCookies(); String getHeader(String key); + + void setAttribute(String key, Object value); + + boolean isContentType(ContentType contentType); } diff --git a/mvc/src/main/java/project/server/mvc/servlet/Request.java b/mvc/src/main/java/project/server/mvc/servlet/Request.java index 161006b..313ec4e 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/Request.java +++ b/mvc/src/main/java/project/server/mvc/servlet/Request.java @@ -1,9 +1,7 @@ package project.server.mvc.servlet; -import java.io.BufferedReader; -import java.io.IOException; -import java.util.ArrayList; import java.util.List; +import project.server.mvc.servlet.http.ContentType; import project.server.mvc.servlet.http.Cookies; import project.server.mvc.servlet.http.HttpHeader; import project.server.mvc.servlet.http.HttpHeaders; @@ -13,19 +11,12 @@ public class Request implements HttpServletRequest { - private static final String EMPTY_STRING = ""; private static final String HOST = "Host"; private final RequestLine requestLine; private final HttpHeaders headers; private final RequestBody requestBody; - public Request(BufferedReader bufferedReader) throws IOException { - this.requestLine = parseRequestLine(bufferedReader); - this.headers = parseHttpHeaders(bufferedReader); - this.requestBody = parseRequestBody(bufferedReader); - } - public Request( RequestLine requestLine, HttpHeaders headers, @@ -36,31 +27,6 @@ public Request( this.requestBody = requestBody; } - private RequestLine parseRequestLine(BufferedReader bufferedReader) throws IOException { - return new RequestLine(bufferedReader.readLine()); - } - - private HttpHeaders parseHttpHeaders(BufferedReader bufferedReader) throws IOException { - List headerLines = new ArrayList<>(); - String headerLine = ""; - - while (!EMPTY_STRING.equals(headerLine = bufferedReader.readLine())) { - headerLines.add(headerLine); - } - return new HttpHeaders(headerLines); - } - - private RequestBody parseRequestBody(BufferedReader bufferedReader) throws IOException { - int contentLength = headers.getContentLength(); - if (contentLength == 0) { - return null; - } - - char[] buffer = new char[contentLength]; - bufferedReader.read(buffer, 0, contentLength); - return new RequestBody(new String(buffer)); - } - @Override public String getHttpVersion() { return requestLine.getHttpVersionAsString(); @@ -78,10 +44,15 @@ public int getContentLength() { } @Override - public String getContentType() { + public ContentType getContentType() { return requestLine.getContentType(); } + @Override + public String getContentTypeAsString() { + return requestLine.getContentTypeAsValue(); + } + @Override public RequestLine getRequestLine() { return requestLine; @@ -103,7 +74,7 @@ public RequestBody getRequestBody() { } @Override - public String getAttribute(String key) { + public Object getAttribute(String key) { return requestBody.getAttribute(key); } @@ -117,6 +88,19 @@ public String getHeader(String key) { return headers.getHeaderValue(key); } + @Override + public void setAttribute( + String key, + Object value + ) { + requestBody.setAttribute(key, value); + } + + @Override + public boolean isContentType(ContentType contentType) { + return this.headers.isContentType(contentType); + } + @Override public String toString() { return String.format("%s%s%s", requestLine, headers, requestBody); diff --git a/mvc/src/main/java/project/server/mvc/servlet/ServletRequest.java b/mvc/src/main/java/project/server/mvc/servlet/ServletRequest.java index 37ce6d6..30a48ad 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/ServletRequest.java +++ b/mvc/src/main/java/project/server/mvc/servlet/ServletRequest.java @@ -1,7 +1,11 @@ package project.server.mvc.servlet; +import project.server.mvc.servlet.http.ContentType; + public interface ServletRequest { int getContentLength(); - String getContentType(); + ContentType getContentType(); + + String getContentTypeAsString(); } diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/ContentType.java b/mvc/src/main/java/project/server/mvc/servlet/http/ContentType.java new file mode 100644 index 0000000..e98dbf8 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/servlet/http/ContentType.java @@ -0,0 +1,40 @@ +package project.server.mvc.servlet.http; + +import java.util.Arrays; +import java.util.function.Predicate; + +public enum ContentType { + TEXT_HTML("html", "text/html"), + TEXT_CSS("css", "text/css"), + TEXT_JS("js", "text/javascript"), + IMAGE_JPEG("jpeg", "image/jpeg"), + IMAGE_PNG("png", "image/png"), + APPLICATION_JSON("application/json", "application/json"), + APPLICATION_OCTET_STREAM("*", "application/octet-stream"); + + private final String type; + private final String value; + + ContentType( + String type, + String value + ) { + this.type = type; + this.value = value; + } + + public static ContentType findByType(String type) { + return Arrays.stream(values()) + .filter(equals(type)) + .findAny() + .orElseGet(() -> APPLICATION_OCTET_STREAM); + } + + private static Predicate equals(String type) { + return contentType -> contentType.value.equals(type); + } + + public String getValue() { + return value; + } +} diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeaders.java b/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeaders.java index 019a0c2..e0d41c8 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeaders.java +++ b/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeaders.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import static java.util.stream.Collectors.joining; public class HttpHeaders { @@ -128,4 +129,15 @@ private String joiningValues(List headers) { .map(HttpHeader::getValue) .collect(joining(HEADER_JOINING_DELIMITER)) + CARRIAGE_RETURN; } + + public boolean isContentType(ContentType contentType) { + List contentTypeHeader = headers.get("Content-Type"); + if (contentTypeHeader == null || contentTypeHeader.isEmpty()) { + return false; + } + Optional findApplicationJson = contentTypeHeader.stream() + .filter(header -> header.getValue().equals(contentType.getValue())) + .findAny(); + return findApplicationJson.isPresent(); + } } diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/HttpMethod.java b/mvc/src/main/java/project/server/mvc/servlet/http/HttpMethod.java index 3b4d354..623a989 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/http/HttpMethod.java +++ b/mvc/src/main/java/project/server/mvc/servlet/http/HttpMethod.java @@ -32,4 +32,8 @@ public boolean isGet() { public boolean isPost() { return this.equals(POST); } + + public boolean isPut() { + return this.equals(PUT); + } } diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/RequestBody.java b/mvc/src/main/java/project/server/mvc/servlet/http/RequestBody.java index 9bb79a7..bee5aad 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/http/RequestBody.java +++ b/mvc/src/main/java/project/server/mvc/servlet/http/RequestBody.java @@ -12,18 +12,41 @@ public class RequestBody { private static final int KEY = 0; private static final int VALUE = 1; - private final Map attributes; + private final Map attributes; public RequestBody(String body) { if (body == null || body.isBlank()) { - attributes = null; + attributes = new HashMap<>(); return; } this.attributes = parseBody(body); } - private Map parseBody(String body) { - Map attributes = new HashMap<>(); + public RequestBody(Map attributes) { + this.attributes = attributes; + } + + public static RequestBody createJsonRequestBody(String body) { + if (body == null || body.isBlank()) { + return new RequestBody(new HashMap<>()); + } + + String parsedBody = body.replaceAll("\"", "") + .replaceAll("\\{", "") + .replaceAll("}", ""); + + Map attribute = new HashMap<>(); + + String[] bodyArray = parsedBody.split(","); + for (String eachBody : bodyArray) { + String[] element = eachBody.split(":"); + attribute.put(element[KEY], element[VALUE]); + } + return new RequestBody(attribute); + } + + private Map parseBody(String body) { + Map attributes = new HashMap<>(); String[] bodyArray = body.split(VALUES_DELIMITER); for (String element : bodyArray) { String[] pair = element.split(VALUE_DELIMITER); @@ -32,7 +55,14 @@ private Map parseBody(String body) { return attributes; } - public String getAttribute(String key) { + public void setAttribute( + String key, + Object value + ) { + attributes.put(key, value); + } + + public Object getAttribute(String key) { return attributes.get(key); } diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/RequestLine.java b/mvc/src/main/java/project/server/mvc/servlet/http/RequestLine.java index 8adfeb9..e090767 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/http/RequestLine.java +++ b/mvc/src/main/java/project/server/mvc/servlet/http/RequestLine.java @@ -18,7 +18,7 @@ public class RequestLine { private final HttpMethod httpMethod; private final RequestUri requestUri; private final HttpVersion httpVersion; - private String contentType; + private ContentType contentType; public RequestLine(String startLine) { String[] startLineArray = startLine.split(DELIMITER); @@ -64,12 +64,12 @@ private boolean isStaticResource(String[] startLineArray) { return startLineArray.length >= 2; } - private String getContentType(String filePath) { + private ContentType getContentType(String filePath) { String contentType = URLConnection.guessContentTypeFromName(filePath); if (contentType == null) { contentType = OCTET_STREAM; } - return contentType; + return ContentType.findByType(contentType); } public HttpMethod getHttpMethod() { @@ -80,10 +80,22 @@ public String getRequestUri() { return requestUri.url(); } - public String getContentType() { + public ContentType getContentType() { return contentType; } + public String getHttpVersionAsString() { + return httpVersion.getValue(); + } + + public String getContentTypeAsValue() { + return contentType.getValue(); + } + + public boolean isDataFormat(ContentType contentType) { + return this.contentType.equals(contentType); + } + @Override public boolean equals(Object object) { if (this == object) { @@ -98,10 +110,6 @@ public boolean equals(Object object) { && httpVersion == that.httpVersion; } - public String getHttpVersionAsString() { - return httpVersion.getValue(); - } - @Override public int hashCode() { return Objects.hash(httpMethod, httpVersion); diff --git a/mvc/src/main/java/project/server/mvc/springframework/annotation/Controller.java b/mvc/src/main/java/project/server/mvc/springframework/annotation/Controller.java index a21498c..d46a433 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/annotation/Controller.java +++ b/mvc/src/main/java/project/server/mvc/springframework/annotation/Controller.java @@ -1,5 +1,14 @@ package project.server.mvc.springframework.annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + @Component +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) public @interface Controller { } diff --git a/mvc/src/main/java/project/server/mvc/springframework/annotation/DeleteMapping.java b/mvc/src/main/java/project/server/mvc/springframework/annotation/DeleteMapping.java new file mode 100644 index 0000000..ca854e2 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/springframework/annotation/DeleteMapping.java @@ -0,0 +1,36 @@ +package project.server.mvc.springframework.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import project.server.mvc.servlet.http.HttpMethod; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@RequestMapping(method = HttpMethod.DELETE) +@Target({ElementType.TYPE_USE, ElementType.METHOD}) +public @interface DeleteMapping { + + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + @AliasFor(annotation = RequestMapping.class) + String path() default ""; + + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; +} diff --git a/mvc/src/main/java/project/server/mvc/springframework/annotation/GetMapping.java b/mvc/src/main/java/project/server/mvc/springframework/annotation/GetMapping.java index f5dd177..83a1062 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/annotation/GetMapping.java +++ b/mvc/src/main/java/project/server/mvc/springframework/annotation/GetMapping.java @@ -20,7 +20,7 @@ String[] value() default {}; @AliasFor(annotation = RequestMapping.class) - String[] path() default {}; + String path() default ""; @AliasFor(annotation = RequestMapping.class) String[] params() default {}; diff --git a/mvc/src/main/java/project/server/mvc/springframework/annotation/PostMapping.java b/mvc/src/main/java/project/server/mvc/springframework/annotation/PostMapping.java new file mode 100644 index 0000000..db2b6e5 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/springframework/annotation/PostMapping.java @@ -0,0 +1,36 @@ +package project.server.mvc.springframework.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import project.server.mvc.servlet.http.HttpMethod; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@RequestMapping(method = HttpMethod.POST) +@Target({ElementType.TYPE_USE, ElementType.METHOD}) +public @interface PostMapping { + + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + @AliasFor(annotation = RequestMapping.class) + String path() default ""; + + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; +} diff --git a/mvc/src/main/java/project/server/mvc/springframework/annotation/PutMapping.java b/mvc/src/main/java/project/server/mvc/springframework/annotation/PutMapping.java new file mode 100644 index 0000000..db3e8d4 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/springframework/annotation/PutMapping.java @@ -0,0 +1,39 @@ +package project.server.mvc.springframework.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import project.server.mvc.servlet.http.HttpMethod; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@RequestMapping(method = HttpMethod.PUT) +@Target({ElementType.TYPE_USE, ElementType.METHOD}) +public @interface PutMapping { + + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + @AliasFor(annotation = RequestMapping.class) + String[] put() default {}; + + @AliasFor(annotation = RequestMapping.class) + String path() default ""; + + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; +} diff --git a/mvc/src/main/java/project/server/mvc/springframework/annotation/RestController.java b/mvc/src/main/java/project/server/mvc/springframework/annotation/RestController.java new file mode 100644 index 0000000..dc99738 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/springframework/annotation/RestController.java @@ -0,0 +1,14 @@ +package project.server.mvc.springframework.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Component +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface RestController { +} diff --git a/mvc/src/main/java/project/server/mvc/springframework/context/ApplicationContext.java b/mvc/src/main/java/project/server/mvc/springframework/context/ApplicationContext.java index 43ff168..4a2cdaa 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/context/ApplicationContext.java +++ b/mvc/src/main/java/project/server/mvc/springframework/context/ApplicationContext.java @@ -1,12 +1,15 @@ package project.server.mvc.springframework.context; +import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import static java.lang.reflect.Modifier.isStatic; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Predicate; @@ -116,12 +119,6 @@ private void registerNamedInstance(Class> clazz) { } } - public static T getBean(String beanName) { - @SuppressWarnings("unchecked") - T bean = (T) ApplicationContext.nameKeyBeans.get(beanName); - return bean; - } - private void add(Class> clazz) throws Exception { if (clazzKeyBeans.containsKey(clazz) || allBeans.contains(clazz)) { return; @@ -195,6 +192,29 @@ public static T getBean(Class clazz) { return clazz.cast(dependencyInjectedBeans.get(clazz)); } + public static void register(T type) { + dependencyInjectedBeans.put(type.getClass(), type); + } + + public static T getBean(String beanName) { + @SuppressWarnings("unchecked") + T bean = (T) ApplicationContext.nameKeyBeans.get(beanName); + return bean; + } + + public static Collection findByAnnotation(Class extends Annotation> clazz) { + List beans = new ArrayList<>(); + for (Object bean : dependencyInjectedBeans.values()) { + Annotation[] annotations = bean.getClass().getAnnotations(); + for (Annotation annotation : annotations) { + if (annotation.annotationType().equals(clazz)) { + beans.add(bean); + } + } + } + return beans; + } + public static Collection getAllDependencyInjectedInstances() { return nameKeyBeans.values(); } diff --git a/mvc/src/main/java/project/server/mvc/springframework/context/ApplicationContextProvider.java b/mvc/src/main/java/project/server/mvc/springframework/context/ApplicationContextProvider.java index 4763c53..f0718fe 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/context/ApplicationContextProvider.java +++ b/mvc/src/main/java/project/server/mvc/springframework/context/ApplicationContextProvider.java @@ -11,8 +11,4 @@ public void setApplicationContext(ApplicationContext applicationContext) { public static ApplicationContext getApplicationContext() { return applicationContext; } - - public static T getBean(String beanName) { - return ApplicationContext.getBean(beanName); - } } diff --git a/mvc/src/main/java/project/server/mvc/springframework/context/ServletConfiguration.java b/mvc/src/main/java/project/server/mvc/springframework/context/DispatcherServletAutoConfiguration.java similarity index 88% rename from mvc/src/main/java/project/server/mvc/springframework/context/ServletConfiguration.java rename to mvc/src/main/java/project/server/mvc/springframework/context/DispatcherServletAutoConfiguration.java index 937ce96..0cd1223 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/context/ServletConfiguration.java +++ b/mvc/src/main/java/project/server/mvc/springframework/context/DispatcherServletAutoConfiguration.java @@ -5,7 +5,7 @@ import project.server.mvc.springframework.web.servlet.DispatcherServlet; @Configuration -public class ServletConfiguration { +public class DispatcherServletAutoConfiguration { @Bean public DispatcherServlet dispatcherServlet() { diff --git a/mvc/src/main/java/project/server/mvc/springframework/handler/HandlerInterceptor.java b/mvc/src/main/java/project/server/mvc/springframework/handler/HandlerInterceptor.java new file mode 100644 index 0000000..ac51f65 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/springframework/handler/HandlerInterceptor.java @@ -0,0 +1,18 @@ +package project.server.mvc.springframework.handler; + +import project.server.mvc.servlet.HttpServletRequest; +import project.server.mvc.servlet.HttpServletResponse; +import project.server.mvc.springframework.web.servlet.ModelAndView; + +public interface HandlerInterceptor { + + default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + return true; + } + + default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { + } + + default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler) { + } +} diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/InterceptorRegistration.java b/mvc/src/main/java/project/server/mvc/springframework/web/InterceptorRegistration.java new file mode 100644 index 0000000..c39615b --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/springframework/web/InterceptorRegistration.java @@ -0,0 +1,36 @@ +package project.server.mvc.springframework.web; + +import java.util.ArrayList; +import java.util.List; +import project.server.mvc.springframework.handler.HandlerInterceptor; + +public class InterceptorRegistration { + + private final List urls = new ArrayList<>(); + private final HandlerInterceptor handlerInterceptor; + + public InterceptorRegistration(HandlerInterceptor handlerInterceptor) { + this.handlerInterceptor = handlerInterceptor; + } + + public List getUrls() { + return urls; + } + + public boolean contains(String url) { + return urls.contains(url); + } + + public HandlerInterceptor getHandlerInterceptor() { + return handlerInterceptor; + } + + public void addPathPatterns(String... url) { + this.urls.addAll(List.of(url)); + } + + @Override + public String toString() { + return String.format("urls:%s, interceptor:%s", urls, handlerInterceptor); + } +} diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/InterceptorRegistry.java b/mvc/src/main/java/project/server/mvc/springframework/web/InterceptorRegistry.java new file mode 100644 index 0000000..3e67630 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/springframework/web/InterceptorRegistry.java @@ -0,0 +1,20 @@ +package project.server.mvc.springframework.web; + +import java.util.ArrayList; +import java.util.List; +import project.server.mvc.springframework.handler.HandlerInterceptor; + +public class InterceptorRegistry { + + private final List registrations = new ArrayList<>(); + + public List getInterceptors() { + return registrations; + } + + public InterceptorRegistration addInterceptor(HandlerInterceptor interceptor) { + InterceptorRegistration registration = new InterceptorRegistration(interceptor); + this.registrations.add(registration); + return registration; + } +} diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/WebMvcConfigurer.java b/mvc/src/main/java/project/server/mvc/springframework/web/WebMvcConfigurer.java new file mode 100644 index 0000000..15ef681 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/springframework/web/WebMvcConfigurer.java @@ -0,0 +1,5 @@ +package project.server.mvc.springframework.web; + +public interface WebMvcConfigurer { + void addInterceptors(InterceptorRegistry registry); +} diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/method/HandlerMethod.java b/mvc/src/main/java/project/server/mvc/springframework/web/method/HandlerMethod.java index 7756f8e..a461cc6 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/method/HandlerMethod.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/method/HandlerMethod.java @@ -2,7 +2,6 @@ import java.lang.reflect.Method; import java.util.Objects; -import project.server.mvc.springframework.web.servlet.resource.ResourceHttpRequestHandler; public class HandlerMethod { @@ -52,10 +51,6 @@ public Method getMethod() { return method; } - public boolean handleStaticResource() { - return handler instanceof ResourceHttpRequestHandler; - } - @Override public boolean equals(Object object) { if (this == object) { diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/DispatcherServlet.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/DispatcherServlet.java index b97c77a..b75e021 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/DispatcherServlet.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/DispatcherServlet.java @@ -1,29 +1,29 @@ package project.server.mvc.springframework.web.servlet; import java.util.List; +import static java.util.List.of; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; import project.server.mvc.servlet.ServletException; +import static project.server.mvc.servlet.http.ContentType.APPLICATION_JSON; +import static project.server.mvc.springframework.context.ApplicationContext.getBean; import project.server.mvc.springframework.web.servlet.mvc.method.RequestMappingHandlerMapping; import project.server.mvc.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; public class DispatcherServlet extends FrameworkServlet { - private final List handlerMappings; - private final List handlerAdapters; - private final List viewResolvers; - private final GlobalExceptionHandler exceptionHandler; + private List handlerMappings; + private List handlerAdapters; + private List viewResolvers; + private GlobalExceptionHandler exceptionHandler; public DispatcherServlet() { - this.handlerMappings = List.of(new RequestMappingHandlerMapping()); - this.handlerAdapters = List.of(new RequestMappingHandlerAdapter()); - this.viewResolvers = List.of(new BeanNameViewResolver()); - this.exceptionHandler = new GlobalExceptionHandler(); + init(); } @Override public void init() { - super.init(); + initStrategies(); } @Override @@ -44,6 +44,9 @@ private void doDispatch( return; } + // ๊ตฌํ ํธ์๋ฅผ ์ํด ๋ฐํํ์ ์ void๋ก ์ง์ + handler.applyPreHandle(request, response); + HandlerAdapter handlerAdapter = getHandlerAdapter(handler.getHandler()); ModelAndView modelAndView = handlerAdapter.handle(request, response, handler.getHandler()); processDispatchResult(request, response, modelAndView); @@ -82,6 +85,12 @@ private void processDispatchResult( HttpServletResponse response, ModelAndView modelAndView ) throws Exception { + if (modelAndView == null) { + if (request.isContentType(APPLICATION_JSON)) { + return; + } + throw new RuntimeException("์ฌ๋ฐ๋ฅด์ง ์์ ์์ฒญ์ ๋๋ค."); + } render(modelAndView, request, response); } @@ -107,6 +116,32 @@ protected View resolveViewName(String viewName) { return null; } + private void initStrategies() { + initHandlerMappings(); + initHandlerAdapters(); + initViewResolvers(); + initExceptionHandler(); + } + + private void initHandlerMappings() { + super.init(); + RequestMappingHandlerMapping bean = getBean(RequestMappingHandlerMapping.class); + this.handlerMappings = of(bean); + } + + private void initHandlerAdapters() { + RequestMappingHandlerAdapter bean = getBean(RequestMappingHandlerAdapter.class); + this.handlerAdapters = of(bean); + } + + private void initViewResolvers() { + this.viewResolvers = of(new BeanNameViewResolver()); + } + + private void initExceptionHandler() { + this.exceptionHandler = new GlobalExceptionHandler(); + } + @Override public void destroy() { super.destroy(); diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/FrameworkServlet.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/FrameworkServlet.java index b935917..6c8ee17 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/FrameworkServlet.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/FrameworkServlet.java @@ -5,24 +5,27 @@ import project.server.mvc.servlet.HttpServletBean; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; +import static project.server.mvc.springframework.context.ApplicationContext.getBean; +import project.server.mvc.springframework.handler.HandlerInterceptor; +import project.server.mvc.springframework.web.InterceptorRegistration; +import project.server.mvc.springframework.web.InterceptorRegistry; import project.server.mvc.springframework.web.method.HandlerMethod; import project.server.mvc.springframework.web.servlet.resource.ResourceHttpRequestHandler; public abstract class FrameworkServlet extends HttpServletBean { - private static final HandlerMethod staticResourceHandlerMethod = new HandlerMethod(new ResourceHttpRequestHandler()); + private final List interceptors; + private final HandlerMethod staticResourceHandlerMethod; private static final String EMPTY_STRING = ""; private static final List staticResources = List.of(".js", ".css", ".favicon", ".jpg", ".jpeg", ".png"); private static final List excludeStaticResources = List.of("my-info.html"); - @Override - public void init() { - super.init(); - } + public FrameworkServlet() { + init(); + InterceptorRegistry registration = getBean(InterceptorRegistry.class); - @Override - public void destroy() { - super.destroy(); + this.interceptors = registration.getInterceptors(); + this.staticResourceHandlerMethod = getBean(HandlerMethod.class); } @Override @@ -37,6 +40,22 @@ public void doGet( processRequest(request, response); } + @Override + protected void doPost( + HttpServletRequest request, + HttpServletResponse response + ) throws Exception { + processRequest(request, response); + } + + @Override + protected void doPut( + HttpServletRequest request, + HttpServletResponse response + ) throws Exception { + processRequest(request, response); + } + private boolean isStaticResource(HttpServletRequest request) { String uri = request.getRequestUri(); String[] parsedUri = uri.split("/"); @@ -61,6 +80,12 @@ private void processStaticRequest( HttpServletResponse response ) throws IOException { ResourceHttpRequestHandler handler = (ResourceHttpRequestHandler) staticResourceHandlerMethod.getHandler(); + for (InterceptorRegistration registration : interceptors) { + if (registration.contains(request.getRequestUri())) { + HandlerInterceptor interceptor = registration.getHandlerInterceptor(); + interceptor.preHandle(request, response, handler); + } + } handler.handleRequest(request, response); } @@ -71,13 +96,5 @@ protected final void processRequest( doService(request, response); } - @Override - protected void doPost( - HttpServletRequest request, - HttpServletResponse response - ) throws Exception { - processRequest(request, response); - } - protected abstract void doService(HttpServletRequest request, HttpServletResponse response) throws Exception; } diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/HandlerExecutionChain.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/HandlerExecutionChain.java index 9d8f858..752501a 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/HandlerExecutionChain.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/HandlerExecutionChain.java @@ -1,11 +1,24 @@ package project.server.mvc.springframework.web.servlet; +import java.util.List; import java.util.Objects; +import project.server.mvc.servlet.HttpServletRequest; +import project.server.mvc.servlet.HttpServletResponse; +import project.server.mvc.springframework.handler.HandlerInterceptor; public class HandlerExecutionChain { + private List interceptors; private final Object handler; + public HandlerExecutionChain( + List interceptors, + Object handler + ) { + this.interceptors = interceptors; + this.handler = handler; + } + public HandlerExecutionChain(Object handler) { this.handler = handler; } @@ -14,6 +27,18 @@ public Object getHandler() { return handler; } + void applyPreHandle( + HttpServletRequest request, + HttpServletResponse response + ) { + if (interceptors == null || interceptors.isEmpty()) { + return; + } + for (HandlerInterceptor interceptor : interceptors) { + interceptor.preHandle(request, response, handler); + } + } + @Override public boolean equals(Object object) { if (this == object) { @@ -33,6 +58,6 @@ public int hashCode() { @Override public String toString() { - return String.format("%s", handler); + return String.format("handler:%s, interceptors:%s", handler, interceptors); } } diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/MyInfoView.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/MyInfoView.java index 3dfa7a9..448d51c 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/MyInfoView.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/MyInfoView.java @@ -51,7 +51,7 @@ private void setResponseHeader( HttpServletResponse response, int lengthOfBodyContent ) { - response.setHeader(CONTENT_TYPE, request.getContentType()); + response.setHeader(CONTENT_TYPE, request.getContentTypeAsString()); response.setHeader(CONTENT_LENGTH, valueOf(lengthOfBodyContent)); } diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMapping.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMapping.java index 90b9c59..89ad8f0 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMapping.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMapping.java @@ -1,22 +1,44 @@ package project.server.mvc.springframework.web.servlet.handler; +import java.util.ArrayList; +import java.util.List; +import project.server.mvc.servlet.HttpServletRequest; +import static project.server.mvc.springframework.context.ApplicationContext.getBean; +import project.server.mvc.springframework.handler.HandlerInterceptor; +import project.server.mvc.springframework.web.InterceptorRegistration; +import project.server.mvc.springframework.web.InterceptorRegistry; import project.server.mvc.springframework.web.servlet.HandlerExecutionChain; import project.server.mvc.springframework.web.servlet.HandlerMapping; -import project.server.mvc.servlet.HttpServletRequest; public abstract class AbstractHandlerMapping implements HandlerMapping { - private Object defaultHandler; + private final List interceptors; + + public AbstractHandlerMapping() { + InterceptorRegistry registration = getBean(InterceptorRegistry.class); + this.interceptors = registration.getInterceptors(); + } @Override public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { Object handler = getHandlerInternal(request); - return new HandlerExecutionChain(handler); - } - - public Object getDefaultHandler() { - return this.defaultHandler; + return getHandlerExecutionChain(handler, request); } protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception; + + protected HandlerExecutionChain getHandlerExecutionChain( + Object handler, + HttpServletRequest request + ) { + List interceptors = new ArrayList<>(); + for (InterceptorRegistration registration : this.interceptors) { + if (registration.contains(request.getRequestUri())) { + HandlerInterceptor interceptor = registration.getHandlerInterceptor(); + interceptors.add(interceptor); + return new HandlerExecutionChain(interceptors, handler); + } + } + return new HandlerExecutionChain(handler); + } } diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java index df3698c..1eb2bbc 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java @@ -1,11 +1,9 @@ package project.server.mvc.springframework.web.servlet.handler; -import java.util.HashMap; import java.util.Map; -import java.util.Optional; +import java.util.Objects; import project.server.mvc.servlet.HttpServletRequest; -import project.server.mvc.servlet.http.HttpMethod; -import project.server.mvc.springframework.context.ApplicationContext; +import static project.server.mvc.springframework.context.ApplicationContext.getBean; import project.server.mvc.springframework.handler.RequestMappingInfo; import project.server.mvc.springframework.web.method.HandlerMethod; @@ -37,33 +35,13 @@ private HandlerMethod lookupHandlerMethod( return registration != null ? registration.handlerMethod : null; } - class MappingRegistry { + static class MappingRegistry { private final Map registry; public MappingRegistry() { - this.registry = new HashMap<>(); - Object homeController = Optional.ofNullable(ApplicationContext.getBean("HomeController")).get(); - Object signUpController = Optional.ofNullable(ApplicationContext.getBean("SignUpController")).get(); - Object loginUpController = Optional.ofNullable(ApplicationContext.getBean("LoginController")).get(); - Object userInfoController = Optional.ofNullable(ApplicationContext.getBean("UserInfoSearchController")).get(); - - registry.put( - new RequestMappingInfo(HttpMethod.GET, "/"), - new MappingRegistration(new HandlerMethod(homeController)) - ); - registry.put( - new RequestMappingInfo(HttpMethod.GET, "/my-info"), - new MappingRegistration(new HandlerMethod(userInfoController)) - ); - registry.put( - new RequestMappingInfo(HttpMethod.POST, "/sign-up"), - new MappingRegistration(new HandlerMethod(signUpController)) - ); - registry.put( - new RequestMappingInfo(HttpMethod.POST, "/sign-in"), - new MappingRegistration(new HandlerMethod(loginUpController)) - ); + HandlerMappingInitializer bean = getBean(HandlerMappingInitializer.class); + this.registry = bean.getRegistry(); } public MappingRegistration getMappingRegistration(RequestMappingInfo requestMappingInfo) { @@ -71,7 +49,7 @@ public MappingRegistration getMappingRegistration(RequestMappingInfo requestMapp } } - public static class MappingRegistration { + static class MappingRegistration { private final HandlerMethod handlerMethod; @@ -82,5 +60,27 @@ public MappingRegistration(HandlerMethod handlerMethod) { public HandlerMethod getHandlerMethod() { return handlerMethod; } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + MappingRegistration that = (MappingRegistration) object; + return handlerMethod.equals(that.handlerMethod); + } + + @Override + public int hashCode() { + return Objects.hash(handlerMethod); + } + + @Override + public String toString() { + return String.format("method: %s", handlerMethod); + } } } diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/HandlerMappingInitializer.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/HandlerMappingInitializer.java new file mode 100644 index 0000000..004d631 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/HandlerMappingInitializer.java @@ -0,0 +1,112 @@ +package project.server.mvc.springframework.web.servlet.handler; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import project.server.mvc.servlet.http.HttpMethod; +import project.server.mvc.springframework.annotation.Controller; +import project.server.mvc.springframework.annotation.DeleteMapping; +import project.server.mvc.springframework.annotation.GetMapping; +import project.server.mvc.springframework.annotation.PostMapping; +import project.server.mvc.springframework.annotation.PutMapping; +import project.server.mvc.springframework.annotation.RestController; +import static project.server.mvc.springframework.context.ApplicationContext.findByAnnotation; +import project.server.mvc.springframework.handler.RequestMappingInfo; +import project.server.mvc.springframework.web.method.HandlerMethod; +import project.server.mvc.springframework.web.servlet.Handler; + +public class HandlerMappingInitializer { + + private Map registry = new HashMap<>(); + + public HandlerMappingInitializer() { + List handlers = new ArrayList<>(); + List restController = findByAnnotation(RestController.class).stream() + .map(bean -> (Handler) bean) + .toList(); + List controller = findByAnnotation(Controller.class).stream() + .map(bean -> (Handler) bean) + .toList(); + + handlers.addAll(controller); + handlers.addAll(restController); + for (Handler handler : handlers) { + registerHandlerMethod(handler); + } + } + + private void registerHandlerMethod(Handler handler) { + Method[] methods = handler.getClass().getDeclaredMethods(); + for (Method method : methods) { + registerGetMapping(handler, method); + registerPostMapping(handler, method); + registerPutMapping(handler, method); + registerDeleteMapping(handler, method); + } + } + + private void registerGetMapping( + Handler handler, + Method method + ) { + if (method.isAnnotationPresent(GetMapping.class)) { + GetMapping getMapping = method.getAnnotation(GetMapping.class); + String url = getMapping.path(); + RequestMappingInfo requestMappingInfo = new RequestMappingInfo(HttpMethod.GET, url); + HandlerMethod handlerMethod = new HandlerMethod(handler); + registerMapping(requestMappingInfo, handlerMethod); + } + } + + private void registerPostMapping( + Handler handler, + Method method + ) { + if (method.isAnnotationPresent(PostMapping.class)) { + PostMapping getMapping = method.getAnnotation(PostMapping.class); + String url = getMapping.path(); + RequestMappingInfo requestMappingInfo = new RequestMappingInfo(HttpMethod.POST, url); + HandlerMethod handlerMethod = new HandlerMethod(handler); + registerMapping(requestMappingInfo, handlerMethod); + } + } + + private void registerPutMapping( + Handler handler, + Method method + ) { + if (method.isAnnotationPresent(PutMapping.class)) { + PutMapping getMapping = method.getAnnotation(PutMapping.class); + String url = getMapping.path(); + RequestMappingInfo requestMappingInfo = new RequestMappingInfo(HttpMethod.PUT, url); + HandlerMethod handlerMethod = new HandlerMethod(handler); + registerMapping(requestMappingInfo, handlerMethod); + } + } + + private void registerDeleteMapping( + Handler handler, + Method method + ) { + if (method.isAnnotationPresent(DeleteMapping.class)) { + DeleteMapping getMapping = method.getAnnotation(DeleteMapping.class); + String url = getMapping.path(); + RequestMappingInfo requestMappingInfo = new RequestMappingInfo(HttpMethod.DELETE, url); + HandlerMethod handlerMethod = new HandlerMethod(handler); + registerMapping(requestMappingInfo, handlerMethod); + } + } + + private void registerMapping( + RequestMappingInfo requestMappingInfo, + HandlerMethod handlerMethod + ) { + registry.put(requestMappingInfo, new AbstractHandlerMethodMapping.MappingRegistration(handlerMethod)); + } + + public Map getRegistry() { + return registry; + } +} diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java index 53011a5..79cbb32 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java @@ -39,6 +39,9 @@ private ModelAndView invokeHandlerMethod( log.info("args: {}", args); Object result = method.invoke(instance, args); log.info("result: {}", result); + if (result == null) { + return null; + } return (ModelAndView) result; } } diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/RequestMappingHandlerMapping.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/RequestMappingHandlerMapping.java index 789b38c..35713f6 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/RequestMappingHandlerMapping.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/RequestMappingHandlerMapping.java @@ -1,15 +1,15 @@ package project.server.mvc.springframework.web.servlet.mvc.method; -import project.server.mvc.springframework.web.servlet.HandlerExecutionChain; -import project.server.mvc.springframework.web.method.HandlerMethod; import project.server.mvc.servlet.HttpServletRequest; +import project.server.mvc.springframework.web.method.HandlerMethod; +import project.server.mvc.springframework.web.servlet.HandlerExecutionChain; public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping { @Override public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { HandlerMethod handlerMethod = getHandlerInternal(request); - return new HandlerExecutionChain(handlerMethod); + return getHandlerExecutionChain(handlerMethod, request); } @Override diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index 900bcf5..d31669a 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -3,8 +3,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; import static project.server.mvc.servlet.http.HttpStatus.NOT_FOUND; @@ -21,6 +19,7 @@ public class ResourceHttpRequestHandler implements HttpRequestHandler { private static final String HTML = ".html"; private static final String STATIC_PREFIX = "static"; + @Override public void handleRequest( HttpServletRequest request, @@ -34,6 +33,7 @@ public void handleRequest( return; } } + InputStream inputStream = getInputStream(getFile(uri)); if (inputStream != null) { response(request, response, inputStream); @@ -89,7 +89,7 @@ private void setResponseHeader( int lengthOfBodyContent ) throws IOException { response.getHttpHeaderLine(); - response.setHeader(CONTENT_TYPE, request.getContentType()); + response.setHeader(CONTENT_TYPE, request.getContentTypeAsString()); response.setHeader(CONTENT_LENGTH, String.valueOf(lengthOfBodyContent)); String header = response.getHttpHeaderLine(); diff --git a/mvc/src/main/java/project/server/mvc/tomcat/AbstractJsseEndpoint.java b/mvc/src/main/java/project/server/mvc/tomcat/AbstractJsseEndpoint.java new file mode 100644 index 0000000..b4c7b92 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/tomcat/AbstractJsseEndpoint.java @@ -0,0 +1,13 @@ +package project.server.mvc.tomcat; + +import java.util.concurrent.ExecutorService; + +public abstract class AbstractJsseEndpoint extends AbstractEndpoint { + + public AbstractJsseEndpoint(ExecutorService executorService) { + super(executorService); + } + + @Override + protected abstract boolean setSocketOptions(U socket); +} diff --git a/mvc/src/main/java/project/server/mvc/tomcat/NioSocketWrapper.java b/mvc/src/main/java/project/server/mvc/tomcat/NioSocketWrapper.java index 93c4f7d..3ca6b76 100644 --- a/mvc/src/main/java/project/server/mvc/tomcat/NioSocketWrapper.java +++ b/mvc/src/main/java/project/server/mvc/tomcat/NioSocketWrapper.java @@ -1,7 +1,6 @@ package project.server.mvc.tomcat; import java.io.IOException; -import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import static java.nio.charset.StandardCharsets.UTF_8; @@ -12,10 +11,12 @@ import project.server.mvc.servlet.HttpServletResponse; import project.server.mvc.servlet.Request; import project.server.mvc.servlet.Response; +import static project.server.mvc.servlet.http.ContentType.APPLICATION_JSON; import project.server.mvc.servlet.http.HttpHeaders; import project.server.mvc.servlet.http.RequestBody; +import static project.server.mvc.servlet.http.RequestBody.createJsonRequestBody; import project.server.mvc.servlet.http.RequestLine; -import static project.server.mvc.springframework.context.ApplicationContextProvider.getBean; +import static project.server.mvc.springframework.context.ApplicationContext.getBean; import project.server.mvc.springframework.web.servlet.DispatcherServlet; @Slf4j @@ -57,7 +58,6 @@ public void run() { try (SocketChannel channel = this.socketChannel) { HttpServletRequest request = createHttpServletRequest(lines, headerLines, requestBody); HttpServletResponse response = new Response(channel); - this.response = response; dispatcherServlet.service(request, response); @@ -67,6 +67,7 @@ public void run() { } } } catch (Exception exception) { + responseError(); log.error("message: {}", exception.getMessage()); } } @@ -90,9 +91,19 @@ private HttpServletRequest createHttpServletRequest( List headerLines, String requestBody ) { + RequestLine requestLine = new RequestLine(lines[START_LINE]); + HttpHeaders httpHeaders = new HttpHeaders(headerLines); + if (httpHeaders.isContentType(APPLICATION_JSON)) { + RequestBody body = createJsonRequestBody(requestBody); + return new Request( + requestLine, + httpHeaders, + body + ); + } return new Request( - new RequestLine(lines[START_LINE]), - new HttpHeaders(headerLines), + requestLine, + httpHeaders, new RequestBody(requestBody) ); } @@ -104,4 +115,15 @@ public HttpServletResponse getResponse() { public void flip() { buffer.flip(); } + + private void responseError() { + ByteBuffer headerBuffer = ByteBuffer.wrap(response.toString().getBytes(UTF_8)); + while (headerBuffer.hasRemaining()) { + try { + this.socketChannel.write(headerBuffer); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } } diff --git a/mvc/src/main/java/project/server/mvc/tomcat/PortFinder.java b/mvc/src/main/java/project/server/mvc/tomcat/PortFinder.java index 0f779e3..6c0cc59 100644 --- a/mvc/src/main/java/project/server/mvc/tomcat/PortFinder.java +++ b/mvc/src/main/java/project/server/mvc/tomcat/PortFinder.java @@ -8,7 +8,7 @@ private PortFinder() { private static final int MIN_PORT_NUMBER = 1; private static final int MAX_PORT_NUMBER = 65535; - private static final int DEFAULT_PORT = 8086; + private static final int DEFAULT_PORT = 8080; public static int findPort(String[] args) { if (args == null || args.length == 0) { diff --git a/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestLineUnitTest.java b/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestLineUnitTest.java index 1987071..6e60159 100644 --- a/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestLineUnitTest.java +++ b/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestLineUnitTest.java @@ -25,7 +25,7 @@ void requestLineParseTest() throws IOException { assertAll( () -> assertEquals(GET, requestLine.getHttpMethod()), () -> assertEquals("/index.html", requestLine.getRequestUri()), - () -> assertEquals("text/html", requestLine.getContentType()), + () -> assertEquals("text/html", requestLine.getContentTypeAsValue()), () -> assertEquals(HTTP_1_1.getValue(), "HTTP/1.1") ); }