diff --git a/.gitignore b/.gitignore index 63d2f1a..edaf5a0 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ out/ ### VS Code ### .vscode/ +*.yml diff --git a/README.md b/README.md index 8ebd4ff..62642bb 100644 --- a/README.md +++ b/README.md @@ -57,19 +57,46 @@ 사용자 정보를 바탕으로 로그인 기능을 구현한다. -1. 로그인 기능을 구현한다. -- 세션을 이용해 구현한다. -- 세션은 애플리케이션 내부에 저장/관리한다. - - 세션 유지 시간을 제한 한다. - - [선택] 최근 로그인 기록과 아이피를 식별할 수 있도록 한다. - -2. 개인 정보 상세 조회 기능을 개발한다. -- [선택] 이미지 업로드 기능을 구현한다. +- [x] 로그인 기능을 구현한다. + - 세션을 이용해 구현한다. + - 세션은 애플리케이션 내부에 저장/관리한다. + - 세션 유지 시간을 제한 한다. + - [선택] 최근 로그인 기록과 아이피를 식별할 수 있도록 한다. + +- [x] 개인 정보 상세 조회 기능을 개발한다. +

### 학습 목표 -- HTTP 특징에 대해 학습한다. + +1. HTTP 특징에 대해 학습한다. - 쿠키/세션에 대해 학습한다. - - 세션 관리 방법에 대해 학습한다. + +2. 세션 관리 방법에 대해 학습한다. + +
+
+
+
+
+
+ +## Step3. 데이터베이스를 교체한다. + +애플리케이션 내부에 저장하던 데이터를 외부 데이터베이스에 저장한다. + +1. 데이터베이스 종류는 자유롭게 선택 한다. + - RDB, Redis 등 +2. JDBC 템플릿을 구현한다. + +
+
+ +### 학습 목표 + +1. 추상화에 대해 이해한다. +2. 데이터베이스 통신 과정에 대해 이해한다. +3. 각 데이터베이스의 특징에 대해 이해한다. +4. 트랜잭션에 대해 학습한다. diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..da23bac --- /dev/null +++ b/app/README.md @@ -0,0 +1,19 @@ +## ⛺️ Application 모듈 + +Application 모듈. + +


+ +## 👪 패키지 간 의존관계 + +Application 모듈은 Mvc, Jdbc 모듈에 의존합니다. + +| Application Module | Mvc Module | Jdbc Module | +|:------------------:|:----------:|:-----------:| +| - | O | O | + +   - Application: 애플리케이션 모듈
+   - Mvc: 스프링 Mvc 모듈
+   - Jdbc: 데이터베이스 접근 모듈
+ +
diff --git a/app/build.gradle b/app/build.gradle index 4bde625..359f72b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,6 +5,10 @@ plugins { dependencies { implementation(project(":mvc")) + implementation(project(":jdbc")) + + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.1") + runtimeOnly("com.mysql:mysql-connector-j:8.1.0") } tasks.named("test") { @@ -68,3 +72,25 @@ sonarqube { property("sonar.coverage.jacoco.xmlReportPaths", "${buildDir}/jacoco/index.xml") } } + +task downloadYml { + doLast { + def url = new URL(System.getenv("YML_URL")) + def connection = url.openConnection() + + def file = new File(projectDir, "./src/main/resources/application.yml") + if (!file.parentFile.exists()) { + file.parentFile.mkdirs() + } + + connection.inputStream.withStream { inputStream -> + file.withOutputStream { outputStream -> + inputStream.transferTo(outputStream) + } + } + } +} + +tasks.named("downloadYml") { + dependsOn downloadYml +} diff --git a/app/src/main/java/project/server/app/common/codeandmessage/failure/ErrorCodeAndMessages.java b/app/src/main/java/project/server/app/common/codeandmessage/failure/ErrorCodeAndMessages.java index e4c1e7c..a490c41 100644 --- a/app/src/main/java/project/server/app/common/codeandmessage/failure/ErrorCodeAndMessages.java +++ b/app/src/main/java/project/server/app/common/codeandmessage/failure/ErrorCodeAndMessages.java @@ -7,7 +7,8 @@ public enum ErrorCodeAndMessages implements ErrorCodeAndMessage { INVALID_SESSION(HttpStatus.UN_AUTHORIZED, "세션이 만료되었습니다."), INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "올바른 값을 입력해주세요."), PAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "페이지를 찾을 수 없습니다."), - UN_AUTHORIZED(HttpStatus.UN_AUTHORIZED, "권한이 존재하지 않습니다."); + UN_AUTHORIZED(HttpStatus.UN_AUTHORIZED, "권한이 존재하지 않습니다."), + INVALID_DATA_ACCESS(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다."); private final HttpStatus httpStatus; private final String errorMessage; diff --git a/app/src/main/java/project/server/app/common/configuration/ConfigMapLoader.java b/app/src/main/java/project/server/app/common/configuration/ConfigMapLoader.java new file mode 100644 index 0000000..0cc165f --- /dev/null +++ b/app/src/main/java/project/server/app/common/configuration/ConfigMapLoader.java @@ -0,0 +1,31 @@ +package project.server.app.common.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.KEBAB_CASE; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import java.io.IOException; +import java.io.InputStream; +import project.server.jdbc.core.ConfigMap; + +public class ConfigMapLoader { + + public ConfigMap getConfigMap() throws IOException { + return loadConfig("application.yml"); + } + + public ConfigMap loadConfig(String resourcePath) throws IOException { + InputStream inputStream = getInputStream(resourcePath); + if (inputStream == null) { + throw new IOException("Resource not found: " + resourcePath); + } + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + mapper.setPropertyNamingStrategy(KEBAB_CASE); + return mapper.readValue(inputStream, ConfigMap.class); + } + + private InputStream getInputStream(String resourcePath) { + return ConfigMapLoader.class.getClassLoader() + .getResourceAsStream(resourcePath); + } +} diff --git a/app/src/main/java/project/server/app/common/configuration/DatabaseConfiguration.java b/app/src/main/java/project/server/app/common/configuration/DatabaseConfiguration.java new file mode 100644 index 0000000..9acd8d6 --- /dev/null +++ b/app/src/main/java/project/server/app/common/configuration/DatabaseConfiguration.java @@ -0,0 +1,35 @@ +package project.server.app.common.configuration; + +import java.io.IOException; +import project.server.jdbc.core.ConfigMap; +import project.server.jdbc.core.DriverManager; +import project.server.jdbc.core.jdbc.JdbcTemplate; +import project.server.jdbc.core.transaction.JdbcTransactionManager; +import project.server.jdbc.core.transaction.PlatformTransactionManager; +import project.server.mvc.springframework.annotation.Bean; +import project.server.mvc.springframework.annotation.Configuration; + +@Configuration +public class DatabaseConfiguration { + + @Bean + public ConfigMapLoader configMapLoader() { + return new ConfigMapLoader(); + } + + @Bean + public DriverManager driverManager() throws IOException { + ConfigMap configMap = configMapLoader().getConfigMap(); + return new DriverManager(configMap); + } + + @Bean + public PlatformTransactionManager transactionManager() throws IOException { + return new JdbcTransactionManager(driverManager().getDataSource()); + } + + @Bean + public JdbcTemplate jdbcTemplate() throws IOException { + return new JdbcTemplate(driverManager().getDataSource()); + } +} diff --git a/app/src/main/java/project/server/app/common/exception/BusinessException.java b/app/src/main/java/project/server/app/common/exception/BusinessException.java index 724899e..d36ca95 100644 --- a/app/src/main/java/project/server/app/common/exception/BusinessException.java +++ b/app/src/main/java/project/server/app/common/exception/BusinessException.java @@ -1,6 +1,7 @@ package project.server.app.common.exception; import project.server.app.common.codeandmessage.ErrorCodeAndMessage; +import project.server.mvc.servlet.http.HttpStatus; public class BusinessException extends RuntimeException { @@ -14,4 +15,14 @@ public BusinessException(ErrorCodeAndMessage errorCodeAndMessage) { public ErrorCodeAndMessage getCodeAndMessage() { return codeAndMessage; } + + @Override + public String toString() { + HttpStatus status = codeAndMessage.getStatus(); + return String.format( + "{\"code\":%d, \"message\":\"%s\"}", + status.getStatusCode(), + codeAndMessage.getErrorMessage() + ); + } } diff --git a/app/src/main/java/project/server/app/common/exception/InvalidParameterException.java b/app/src/main/java/project/server/app/common/exception/InvalidParameterException.java index 39b20bd..38b49b3 100644 --- a/app/src/main/java/project/server/app/common/exception/InvalidParameterException.java +++ b/app/src/main/java/project/server/app/common/exception/InvalidParameterException.java @@ -1,6 +1,7 @@ package project.server.app.common.exception; import project.server.app.common.codeandmessage.failure.ErrorCodeAndMessages; +import project.server.mvc.servlet.http.HttpStatus; public class InvalidParameterException extends RuntimeException { @@ -19,4 +20,14 @@ public ErrorCodeAndMessages getErrorCodeAndMessages() { public Object getArgs() { return args; } + + @Override + public String toString() { + HttpStatus status = errorCodeAndMessages.getStatus(); + return String.format( + "{\"code\":%d, \"message\":\"%s\"}", + status.getStatusCode(), + errorCodeAndMessages.getErrorMessage() + ); + } } diff --git a/app/src/main/java/project/server/app/common/exception/UnAuthorizedException.java b/app/src/main/java/project/server/app/common/exception/UnAuthorizedException.java index 9b0981b..c74d84b 100644 --- a/app/src/main/java/project/server/app/common/exception/UnAuthorizedException.java +++ b/app/src/main/java/project/server/app/common/exception/UnAuthorizedException.java @@ -2,9 +2,9 @@ import static project.server.app.common.codeandmessage.failure.ErrorCodeAndMessages.UN_AUTHORIZED; -public class UnAuthorizedException extends RuntimeException { +public class UnAuthorizedException extends BusinessException { public UnAuthorizedException() { - super(UN_AUTHORIZED.getErrorMessage()); + super(UN_AUTHORIZED); } } diff --git a/app/src/main/java/project/server/app/common/utils/JdbcConfiguration.java b/app/src/main/java/project/server/app/common/utils/JdbcConfiguration.java new file mode 100644 index 0000000..4e47c03 --- /dev/null +++ b/app/src/main/java/project/server/app/common/utils/JdbcConfiguration.java @@ -0,0 +1,7 @@ +package project.server.app.common.utils; + +import project.server.mvc.springframework.annotation.Configuration; + +@Configuration +public class JdbcConfiguration { +} diff --git a/app/src/main/java/project/server/app/core/domain/user/User.java b/app/src/main/java/project/server/app/core/domain/user/User.java index 4a2c59d..2f53391 100644 --- a/app/src/main/java/project/server/app/core/domain/user/User.java +++ b/app/src/main/java/project/server/app/core/domain/user/User.java @@ -60,22 +60,27 @@ public String getPassword() { return password.value(); } - public Deleted getDeleted() { - return deleted; + public LocalDateTime getCreatedAt() { + return createdAt; } - public boolean isNew() { - return this.id == null; + public LocalDateTime getLastModifiedAt() { + return lastModifiedAt; } - public void registerId(Long id) { - this.id = id; + public Deleted getDeleted() { + return deleted; } public boolean isAlreadyDeleted() { return this.deleted.equals(Deleted.TRUE); } + public void delete(LocalDateTime lastModifiedAt) { + this.lastModifiedAt = lastModifiedAt; + this.deleted = Deleted.TRUE; + } + @Override public boolean equals(Object object) { if (this == object) { @@ -87,11 +92,6 @@ public boolean equals(Object object) { return getId().equals(user.getId()); } - public void delete(LocalDateTime lastModifiedAt) { - this.lastModifiedAt = lastModifiedAt; - this.deleted = Deleted.TRUE; - } - @Override public int hashCode() { return Objects.hash(getId()); 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 1bf5f38..600ba44 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 @@ -4,15 +4,17 @@ import java.util.Optional; public interface UserRepository { - User save(User user); + Long save(User user); Optional findById(Long userId); void clear(); - boolean existByName(String username); - - List findAll(); + boolean existsByName(String username); Optional findByUsernameAndPassword(String username, String password); + + void delete(User user); + + List findAll(); } diff --git a/app/src/main/java/project/server/app/core/web/user/application/UserSaveUseCase.java b/app/src/main/java/project/server/app/core/web/user/application/UserSaveUseCase.java index 34c57ab..bc48e46 100644 --- a/app/src/main/java/project/server/app/core/web/user/application/UserSaveUseCase.java +++ b/app/src/main/java/project/server/app/core/web/user/application/UserSaveUseCase.java @@ -1,7 +1,5 @@ package project.server.app.core.web.user.application; -import project.server.app.core.domain.user.User; - public interface UserSaveUseCase { - User save(User user); + Long save(String username, String password); } diff --git a/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginService.java b/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginService.java index 76b1eea..7c8d7ff 100644 --- a/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginService.java +++ b/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginService.java @@ -39,12 +39,16 @@ public Session login( @Override public Session findSessionById(Long userId) { - Session findSession = sessionManager.findByUserId(userId) - .orElseThrow(UnAuthorizedException::new); + try { + Session findSession = sessionManager.findByUserId(userId) + .orElseThrow(UnAuthorizedException::new); - if (!findSession.isValid(now())) { - throw new SessionExpiredException(); + if (!findSession.isValid(now())) { + throw new SessionExpiredException(); + } + return findSession; + } catch (UnAuthorizedException | SessionExpiredException exception) { + return null; } - return findSession; } } diff --git a/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginServiceProxy.java b/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginServiceProxy.java new file mode 100644 index 0000000..fe27ffd --- /dev/null +++ b/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginServiceProxy.java @@ -0,0 +1,61 @@ +package project.server.app.core.web.user.application.service; + +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; +import project.server.app.common.configuration.DatabaseConfiguration; +import project.server.app.common.exception.BusinessException; +import project.server.app.common.login.Session; +import project.server.app.core.web.user.application.UserLoginUseCase; +import project.server.jdbc.core.exception.DataAccessException; +import static project.server.jdbc.core.transaction.DefaultTransactionDefinition.createTransactionDefinition; +import project.server.jdbc.core.transaction.PlatformTransactionManager; +import project.server.jdbc.core.transaction.TransactionStatus; +import project.server.mvc.springframework.annotation.Component; + +@Slf4j +@Component +public class UserLoginServiceProxy implements UserLoginUseCase { + + private final PlatformTransactionManager txManager; + private final UserLoginService target; + + public UserLoginServiceProxy( + DatabaseConfiguration dbConfiguration, + UserLoginService target + ) throws IOException { + this.txManager = dbConfiguration.transactionManager(); + this.target = target; + } + + @Override + public Session login( + String username, + String password + ) { + TransactionStatus txStatus = getTransactionStatus(false); + log.debug("txStatus:{}", txStatus); + try { + Session findSession = target.login(username, password); + txManager.commit(txStatus); + return findSession; + } catch (BusinessException | DataAccessException exception) { + log.error("{}", exception.getMessage()); + txManager.rollback(txStatus); + throw exception; + } + } + + @Override + public Session findSessionById(Long userId) { + return target.findSessionById(userId); + } + + private TransactionStatus getTransactionStatus(boolean readOnly) { + try { + return txManager.getTransaction(createTransactionDefinition(readOnly)); + } catch (Exception exception) { + log.error("{}", exception.getMessage()); + throw new DataAccessException(); + } + } +} 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 38e9e2a..11c4f63 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 @@ -21,8 +21,12 @@ public UserService(UserRepository userRepository) { } @Override - public User save(User user) { - boolean duplicatedUser = userRepository.existByName(user.getUsername()); + public Long save( + String username, + String password + ) { + User user = new User(username, password); + boolean duplicatedUser = userRepository.existsByName(user.getUsername()); if (duplicatedUser) { throw new DuplicatedUsernameException(); } @@ -31,8 +35,12 @@ public User save(User user) { @Override public User findById(Long userId) { - return userRepository.findById(userId) + User findUser = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); + if (findUser.isAlreadyDeleted()) { + throw new UserNotFoundException(); + } + return findUser; } @Override @@ -45,5 +53,6 @@ public void delete(LoginUser loginUser) { } findUser.delete(LocalDateTime.now()); + userRepository.delete(findUser); } } 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 new file mode 100644 index 0000000..d25301c --- /dev/null +++ b/app/src/main/java/project/server/app/core/web/user/application/service/UserServiceProxy.java @@ -0,0 +1,96 @@ +package project.server.app.core.web.user.application.service; + +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; +import project.server.app.common.configuration.DatabaseConfiguration; +import project.server.app.common.exception.BusinessException; +import project.server.app.common.exception.InvalidParameterException; +import project.server.app.common.login.LoginUser; +import project.server.app.core.domain.user.User; +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.jdbc.core.exception.DataAccessException; +import static project.server.jdbc.core.transaction.DefaultTransactionDefinition.createTransactionDefinition; +import project.server.jdbc.core.transaction.PlatformTransactionManager; +import project.server.jdbc.core.transaction.TransactionStatus; +import project.server.mvc.springframework.annotation.Component; + +@Slf4j +@Component +public class UserServiceProxy implements UserSaveUseCase, UserSearchUseCase, UserDeleteUseCase { + + private final PlatformTransactionManager txManager; + private final UserService target; + + public UserServiceProxy( + DatabaseConfiguration dbConfiguration, + UserService target + ) throws IOException { + this.txManager = dbConfiguration.transactionManager(); + this.target = target; + } + + @Override + public Long save( + String username, + String password + ) { + TransactionStatus txStatus = getTransactionStatus(false); + log.debug("txStatus:[{}]", txStatus.getTransaction()); + try { + Long userId = target.save(username, password); + txManager.commit(txStatus); + log.debug("Transaction finished."); + return userId; + } catch (IllegalArgumentException exception) { + txManager.rollback(txStatus); + log.error("{}", exception.getMessage()); + throw new InvalidParameterException(); + } catch (BusinessException | DataAccessException exception) { + txManager.rollback(txStatus); + log.error("{}", exception.getMessage()); + throw exception; + } + } + + @Override + public User findById(Long userId) { + TransactionStatus txStatus = getTransactionStatus(true); + log.debug("txStatus:[{}]", txStatus.getTransaction()); + try { + User findUser = target.findById(userId); + txManager.commit(txStatus); + log.debug("Transaction finished."); + return findUser; + } catch (BusinessException | DataAccessException exception) { + txManager.rollback(txStatus); + log.error("{}", exception.getMessage()); + throw exception; + } + } + + @Override + public void delete(LoginUser loginUser) { + TransactionStatus txStatus = getTransactionStatus(false); + log.debug("txStatus:[{}]", txStatus.getTransaction()); + try { + target.delete(loginUser); + txManager.commit(txStatus); + log.debug("Transaction finished."); + } catch (BusinessException | DataAccessException exception) { + txManager.rollback(txStatus); + log.error("{}", exception.getMessage()); + throw exception; + } + } + + private TransactionStatus getTransactionStatus(boolean readOnly) { + try { + return txManager.getTransaction(createTransactionDefinition(readOnly)); + } catch (Exception exception) { + log.error("{}", exception.getMessage()); + throw new DataAccessException(); + } + } +} 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 a0fa24b..3f73e41 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 @@ -1,64 +1,78 @@ package project.server.app.core.web.user.persistence; -import static java.lang.Boolean.FALSE; -import static java.lang.Boolean.TRUE; +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import static java.sql.Statement.RETURN_GENERATED_KEYS; +import java.sql.Timestamp; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Predicate; +import static java.util.Optional.empty; +import static java.util.Optional.ofNullable; +import project.server.app.common.configuration.DatabaseConfiguration; import project.server.app.core.domain.user.User; import project.server.app.core.domain.user.UserRepository; -import project.server.app.core.web.user.exception.AlreadyRegisteredUserException; +import project.server.jdbc.core.exception.DataAccessException; +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 project.server.jdbc.core.jdbc.JdbcTemplate; +import project.server.jdbc.core.jdbc.RowMapper; import project.server.mvc.springframework.annotation.Repository; @Repository public class UserPersistenceRepository implements UserRepository { - private static final Boolean ALREADY_EXIST = TRUE; - private static final Boolean NOT_FOUND = FALSE; + private final JdbcTemplate jdbcTemplate; + private final RowMapper rowMapper; - private static final Map factory = new ConcurrentHashMap<>(); - private static final Lock lock = new ReentrantLock(); - private static final AtomicLong idGenerator = new AtomicLong(0L); + private UserPersistenceRepository( + DatabaseConfiguration dbConfig, + RowMapper rowMapper + ) throws IOException { + this.jdbcTemplate = dbConfig.jdbcTemplate(); + this.rowMapper = rowMapper; + } @Override - public User save(User user) { - lock.lock(); - try { - Long id = idGenerator.incrementAndGet(); - if (!user.isNew()) { - throw new AlreadyRegisteredUserException(); + public Long save(User user) { + String sql = insert(); + return jdbcTemplate.queryForObject(connection -> { + PreparedStatement pstmt = connection.prepareStatement(sql, RETURN_GENERATED_KEYS); + pstmt.setString(1, user.getUsername()); + pstmt.setString(2, user.getPassword()); + pstmt.setTimestamp(3, Timestamp.valueOf(user.getCreatedAt())); + pstmt.setTimestamp(4, (user.getLastModifiedAt() != null) ? Timestamp.valueOf(user.getLastModifiedAt()) : null); + pstmt.setObject(5, user.getDeleted().toString()); + pstmt.executeUpdate(); + try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) { + if (generatedKeys.next()) { + return generatedKeys.getLong(1); + } else { + throw new DataAccessException(); + } } - user.registerId(id); - factory.put(id, user); - } finally { - lock.unlock(); - } - return user; + }); } @Override public Optional findById(Long userId) { - return Optional.ofNullable(factory.get(userId)); - } - - @Override - public boolean existByName(String username) { - User findUser = factory.values().stream() - .filter(equalsUsername(username)) - .findAny() - .orElseGet(() -> null); - return findUser != null ? ALREADY_EXIST : NOT_FOUND; + String sql = selectBy(User.class); + return jdbcTemplate.queryForObject(sql, pstmt -> + pstmt.setLong(1, userId), + rs -> rs.next() ? ofNullable(rowMapper.mapRow(rs)) : empty() + ); } @Override - public List findAll() { - return factory.values().stream() - .toList(); + public boolean existsByName(String username) { + String sql = selectBy(User.class, "username"); + return jdbcTemplate.queryForObject(sql, pstmt -> + pstmt.setString(1, username), + rs -> rs.next() && rs.getInt(1) > 0 + ); } @Override @@ -66,25 +80,33 @@ public Optional findByUsernameAndPassword( String username, String password ) { - return Optional.ofNullable( - factory.values().stream() - .filter(equalsUsername(username)) - .filter(equalsPassword(password)) - .findAny() - .orElseGet(() -> null) + String sql = selectBy(User.class, "username", "password"); + return jdbcTemplate.queryForObject(sql, pstmt -> { + pstmt.setString(1, username); + pstmt.setString(2, password); + }, rs -> rs.next() ? ofNullable(rowMapper.mapRow(rs)) : empty() ); } @Override - public void clear() { - factory.clear(); + public void delete(User user) { + String sql = update(User.class, "deleted"); + jdbcTemplate.queryForObject(sql, pstmt -> { + pstmt.setLong(1, user.getId()); + pstmt.executeUpdate(); + return null; + }); } - private Predicate equalsUsername(String username) { - return user -> user.getUsername().equals(username); + @Override + public List findAll() { + String sql = selectAll(User.class); + return jdbcTemplate.queryForList(sql, rowMapper); } - private Predicate equalsPassword(String password) { - return user -> user.getPassword().equals(password); + @Override + public void clear() { + String sql = truncate(User.class); + jdbcTemplate.execute(sql); } } 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 new file mode 100644 index 0000000..83b6361 --- /dev/null +++ b/app/src/main/java/project/server/app/core/web/user/persistence/UserRowMapper.java @@ -0,0 +1,32 @@ +package project.server.app.core.web.user.persistence; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDateTime; +import project.server.app.core.domain.user.Deleted; +import project.server.app.core.domain.user.User; +import project.server.jdbc.core.jdbc.RowMapper; +import project.server.mvc.springframework.annotation.Component; + +@Component +public class UserRowMapper implements RowMapper { + + private static final String USER_ID = "id"; + private static final String USERNAME = "username"; + private static final String PASSWORD = "password"; + private static final String CREATED_AT = "created_at"; + private static final String LAST_MODIFIED_AT = "last_modified_at"; + private static final String DELETED = "deleted"; + + @Override + public User mapRow(ResultSet rs) throws SQLException { + Long id = rs.getLong(USER_ID); + 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; + 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/LoginController.java b/app/src/main/java/project/server/app/core/web/user/presentation/LoginController.java index a08310a..7b5f02e 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 @@ -6,16 +6,18 @@ 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.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.web.servlet.Handler; import project.server.mvc.springframework.web.servlet.ModelAndView; -import project.server.mvc.servlet.http.Cookie; -import static project.server.mvc.servlet.http.HttpStatus.OK; @Slf4j @Controller public class LoginController implements Handler { + private static final String MAX_AGE = "; Max-Age=900"; + private final UserValidator validator; private final UserLoginUseCase userLoginUseCase; @@ -47,8 +49,8 @@ private void setResponse( HttpServletResponse response, Session session ) { - Cookie cookie = new Cookie("sessionId", session.getUserIdAsString()); + Cookie cookie = new Cookie("sessionId", session.getUserIdAsString() + MAX_AGE); response.addCookie(cookie); - response.setStatus(OK); + response.setStatus(MOVE_PERMANENTLY); } } 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 859e7e4..efa1955 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 @@ -1,7 +1,6 @@ package project.server.app.core.web.user.presentation; import lombok.extern.slf4j.Slf4j; -import project.server.app.core.domain.user.User; import project.server.app.core.web.user.application.UserSaveUseCase; import project.server.app.core.web.user.presentation.validator.UserValidator; import project.server.mvc.servlet.HttpServletRequest; @@ -38,7 +37,7 @@ public ModelAndView process( log.info("username: {}, password: {}", username, password); validator.validateLoginInfo(username, password); - userSaveUseCase.save(new User(username, password)); + userSaveUseCase.save(username, password); response.setStatus(OK); return new ModelAndView("redirect:/index.html"); 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 9ef517a..d82de8c 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 @@ -38,7 +38,7 @@ public ModelAndView process( HttpServletResponse response ) { Long sessionId = getSessionId(request.getCookies()); - validator.validateSessionId(sessionId); + validator.validateSessionId(sessionId, response); Session findSession = loginUseCase.findSessionById(sessionId); log.info("Session:{}", findSession); 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 f19db0f..593022d 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 @@ -1,9 +1,9 @@ package project.server.app.core.web.user.presentation; import lombok.extern.slf4j.Slf4j; -import static project.server.app.common.utils.HeaderUtils.getSessionId; 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; @@ -44,9 +44,11 @@ public ModelAndView process( HttpServletResponse response ) { Long sessionId = getSessionId(request.getCookies()); - validator.validateSessionId(sessionId); + validator.validateSessionId(sessionId, response); Session findSession = loginUseCase.findSessionById(sessionId); + validator.validateSession(findSession, response); + log.info("Session:{}", findSession); LoginUser loginUser = new LoginUser(findSession); diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/validator/UserValidator.java b/app/src/main/java/project/server/app/core/web/user/presentation/validator/UserValidator.java index 427c7c7..51ee568 100644 --- a/app/src/main/java/project/server/app/core/web/user/presentation/validator/UserValidator.java +++ b/app/src/main/java/project/server/app/core/web/user/presentation/validator/UserValidator.java @@ -1,12 +1,24 @@ package project.server.app.core.web.user.presentation.validator; +import lombok.extern.slf4j.Slf4j; import project.server.app.common.exception.InvalidParameterException; import project.server.app.common.exception.UnAuthorizedException; +import project.server.app.common.login.Session; +import project.server.mvc.servlet.HttpServletResponse; +import project.server.mvc.servlet.http.Cookie; +import static project.server.mvc.servlet.http.HttpStatus.UN_AUTHORIZED; import project.server.mvc.springframework.annotation.Component; +@Slf4j @Component public class UserValidator { + private static final String CACHE_CONTROL = "cache-control"; + private static final String COOKIE_DELIMITER = "; "; + private static final String SESSION_ID = "sessionId"; + private static final String EMPTY_STRING = ""; + private static final String MAX_AGE = "max-age=0"; + public void validateSignUpInfo( String username, String password @@ -31,9 +43,32 @@ public void validateLoginInfo( } } - public void validateSessionId(Long userId) { + public void validateSessionId( + Long userId, + HttpServletResponse response + ) { if (userId == null) { + setInvalidSession(response); + log.error("InvalidSession. UserId: {}", userId); throw new UnAuthorizedException(); } } + + public void validateSession( + Session session, + HttpServletResponse response + ) { + if (session == null) { + setInvalidSession(response); + log.error("InvalidSession session. Session is null."); + throw new UnAuthorizedException(); + } + } + + private void setInvalidSession(HttpServletResponse response) { + Cookie cookie = new Cookie(SESSION_ID, EMPTY_STRING + COOKIE_DELIMITER + MAX_AGE); + response.addCookie(cookie); + response.setHeader(CACHE_CONTROL, MAX_AGE); + response.setStatus(UN_AUTHORIZED); + } } diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/resources/static/index.html b/app/src/main/resources/static/index.html index 234d0c8..75ffbb3 100644 --- a/app/src/main/resources/static/index.html +++ b/app/src/main/resources/static/index.html @@ -163,9 +163,9 @@

Infra

const sessionId = getCookie('sessionId'); localStorage.setItem("logined", sessionId ? "true" : "false"); - var logined = localStorage.getItem("logined"); + const logined = localStorage.getItem("logined"); if (logined === "true") { - var signInMenu = document.querySelector('.navbar__menu li a[href="/sign-in.html"]'); + const signInMenu = document.querySelector('.navbar__menu li a[href="/sign-in.html"]'); if (signInMenu) { signInMenu.textContent = "My"; signInMenu.setAttribute('href', '/my-info.html'); diff --git a/app/src/main/resources/static/my-info.html b/app/src/main/resources/static/my-info.html index 401fe79..ff75362 100644 --- a/app/src/main/resources/static/my-info.html +++ b/app/src/main/resources/static/my-info.html @@ -101,9 +101,9 @@

회원 상세정보

}, false); window.addEventListener('DOMContentLoaded', (event) => { - fetch('http://localhost:8086/user-info.index.html') + fetch('http://localhost:8086/my-info.html') .then(response => { - if (response.status === 401) { + if (response.status === 301) { window.location.href = '/'; } }) 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 9e6ae1a..ec7c126 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,6 @@ package project.server.app.test.integrationtest; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; 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 +21,7 @@ protected IntegrationTestBase() { } } - @BeforeEach + @AfterEach void setUp() { userRepository.clear(); } diff --git a/app/src/test/java/project/server/app/test/integrationtest/user/UserDeleteIntegrationTest.java b/app/src/test/java/project/server/app/test/integrationtest/user/UserDeleteIntegrationTest.java index cf582aa..3367346 100644 --- a/app/src/test/java/project/server/app/test/integrationtest/user/UserDeleteIntegrationTest.java +++ b/app/src/test/java/project/server/app/test/integrationtest/user/UserDeleteIntegrationTest.java @@ -3,14 +3,13 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import project.server.app.common.exception.BusinessException; import static project.server.app.common.fixture.user.UserFixture.createUser; import project.server.app.common.login.LoginUser; import project.server.app.core.domain.user.User; 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.service.UserService; +import project.server.app.core.web.user.application.service.UserServiceProxy; import project.server.app.core.web.user.exception.UserNotFoundException; import project.server.app.test.integrationtest.IntegrationTestBase; import static project.server.mvc.springframework.context.ApplicationContext.getBean; @@ -18,19 +17,20 @@ @DisplayName("[IntegrationTest] 사용자 삭제 통합 테스트") class UserDeleteIntegrationTest extends IntegrationTestBase { - private final UserSaveUseCase userSaveUseCase = getBean(UserService.class); - private final UserSearchUseCase userSearchUseCase = getBean(UserService.class); - private final UserDeleteUseCase userDeleteUseCase = getBean(UserService.class); + private final UserSaveUseCase userSaveUseCase = getBean(UserServiceProxy.class); + private final UserSearchUseCase userSearchUseCase = getBean(UserServiceProxy.class); + private final UserDeleteUseCase userDeleteUseCase = getBean(UserServiceProxy.class); @Test @DisplayName("삭제된 사용자를 삭제하려하면 UserNotFoundException이 발생한다.") - void test() { - User savedUser = userSaveUseCase.save(createUser()); - LoginUser loginUser = new LoginUser(savedUser.getId(), null); + void alreadyDeletedUserDeleteTest() { + User newUser = createUser(); + Long userId = userSaveUseCase.save(newUser.getUsername(), newUser.getPassword()); + LoginUser loginUser = new LoginUser(userId, null); userDeleteUseCase.delete(loginUser); assertThatThrownBy(() -> userDeleteUseCase.delete(loginUser)) - .isInstanceOf(BusinessException.class) + .isInstanceOf(RuntimeException.class) .isExactlyInstanceOf(UserNotFoundException.class) .hasMessage("사용자를 찾을 수 없습니다."); } diff --git a/app/src/test/java/project/server/app/test/integrationtest/user/UserLoginIntegrationTest.java b/app/src/test/java/project/server/app/test/integrationtest/user/UserLoginIntegrationTest.java index f94a117..b87812f 100644 --- a/app/src/test/java/project/server/app/test/integrationtest/user/UserLoginIntegrationTest.java +++ b/app/src/test/java/project/server/app/test/integrationtest/user/UserLoginIntegrationTest.java @@ -1,7 +1,10 @@ package project.server.app.test.integrationtest.user; +import static java.lang.Long.MAX_VALUE; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import project.server.app.common.exception.UnAuthorizedException; @@ -9,22 +12,26 @@ 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.UserSaveUseCase; -import project.server.app.core.web.user.application.service.UserLoginService; -import project.server.app.core.web.user.application.service.UserService; +import project.server.app.core.web.user.application.UserSearchUseCase; +import project.server.app.core.web.user.application.service.UserLoginServiceProxy; +import project.server.app.core.web.user.application.service.UserServiceProxy; import project.server.app.test.integrationtest.IntegrationTestBase; import static project.server.mvc.springframework.context.ApplicationContext.getBean; @DisplayName("[IntegrationTest] 로그인 통합 테스트") class UserLoginIntegrationTest extends IntegrationTestBase { - private final UserSaveUseCase userSaveUseCase = getBean(UserService.class); - private final UserLoginUseCase loginUseCase = getBean(UserLoginService.class); + private final UserSaveUseCase userSaveUseCase = getBean(UserServiceProxy.class); + private final UserSearchUseCase userSearchUseCase = getBean(UserServiceProxy.class); + private final UserLoginUseCase loginUseCase = getBean(UserLoginServiceProxy.class); @Test @DisplayName("정상적으로 로그인이 되면 세션이 발급된다.") void sessionCreateTest() { - User savedUser = userSaveUseCase.save(new User("Steve-Jobs", "Helloworld")); - Session session = loginUseCase.login(savedUser.getUsername(), savedUser.getPassword()); + User newUser = new User("Steve-Jobs", "Helloworld"); + Long userId = userSaveUseCase.save(newUser.getUsername(), newUser.getPassword()); + User findUser = userSearchUseCase.findById(userId); + Session session = loginUseCase.login(findUser.getUsername(), findUser.getPassword()); assertNotNull(session); } @@ -33,20 +40,18 @@ void sessionCreateTest() { @DisplayName("세션이 존재하면 이를 조회할 수 있다.") void sessionSearchTest() { User newUser = new User("Steve-Jobs", "Helloworld"); - User savedUser = userSaveUseCase.save(newUser); - Session session = loginUseCase.login(savedUser.getUsername(), savedUser.getPassword()); + Long userId = userSaveUseCase.save(newUser.getUsername(), newUser.getPassword()); + User findUser = userSearchUseCase.findById(userId); + Session session = loginUseCase.login(findUser.getUsername(), findUser.getPassword()); assertNotNull(loginUseCase.findSessionById(session.userId())); } @Test - @DisplayName("세션이 존재하지 않으면 UnAuthorizedException이 발생한다.") + @DisplayName("세션이 존재하지 않으면 null 값이 반환된다.") void sessionSearchFailureTest() { - Long invalidSessionId = Long.MAX_VALUE; + Long invalidSessionId = MAX_VALUE; - assertThatThrownBy(() -> loginUseCase.findSessionById(invalidSessionId)) - .isInstanceOf(RuntimeException.class) - .isExactlyInstanceOf(UnAuthorizedException.class) - .hasMessage("권한이 존재하지 않습니다."); + assertNull(loginUseCase.findSessionById(invalidSessionId)); } } diff --git a/app/src/test/java/project/server/app/test/integrationtest/user/UserSaveIntegrationTest.java b/app/src/test/java/project/server/app/test/integrationtest/user/UserSaveIntegrationTest.java index 1e53a64..3494910 100644 --- a/app/src/test/java/project/server/app/test/integrationtest/user/UserSaveIntegrationTest.java +++ b/app/src/test/java/project/server/app/test/integrationtest/user/UserSaveIntegrationTest.java @@ -1,25 +1,15 @@ package project.server.app.test.integrationtest.user; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import lombok.extern.slf4j.Slf4j; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import project.server.app.common.exception.BusinessException; import project.server.app.core.domain.user.User; -import project.server.app.core.domain.user.UserRepository; import project.server.app.core.web.user.application.UserSaveUseCase; -import project.server.app.core.web.user.application.service.UserService; -import project.server.app.core.web.user.exception.AlreadyRegisteredUserException; +import project.server.app.core.web.user.application.service.UserServiceProxy; import project.server.app.core.web.user.exception.DuplicatedUsernameException; -import project.server.app.core.web.user.persistence.UserPersistenceRepository; import project.server.app.test.integrationtest.IntegrationTestBase; import static project.server.mvc.springframework.context.ApplicationContext.getBean; @@ -27,28 +17,27 @@ @DisplayName("[IntegrationTest] 사용자 저장 통합 테스트") class UserSaveIntegrationTest extends IntegrationTestBase { - private final UserSaveUseCase userSaveUseCase = getBean(UserService.class); - private final UserRepository userRepository = getBean(UserPersistenceRepository.class); + private final UserSaveUseCase userSaveUseCase = getBean(UserServiceProxy.class); @Test @DisplayName("사용자가 저장되면 PK가 생성된다.") void userSaveTest() { User newUser = new User("Steve-Jobs", "helloworld"); - User savedUser = userSaveUseCase.save(newUser); + Long userId = userSaveUseCase.save(newUser.getUsername(), newUser.getPassword()); - assertNotNull(savedUser.getId()); + assertNotNull(userId); } @Test @DisplayName("사용자가 이미 저장 돼 있다면 AlreadyRegisteredUserException이 발생한다.") void userSaveFailureTest() { User newUser = new User("Steve-Jobs", "helloworld"); - userSaveUseCase.save(newUser); + userSaveUseCase.save(newUser.getUsername(), newUser.getPassword()); - assertThatThrownBy(() -> userRepository.save(newUser)) + assertThatThrownBy(() -> userSaveUseCase.save(newUser.getUsername(), newUser.getPassword())) .isInstanceOf(BusinessException.class) - .isExactlyInstanceOf(AlreadyRegisteredUserException.class) - .hasMessage("이미 가입된 사용자 입니다."); + .isExactlyInstanceOf(DuplicatedUsernameException.class) + .hasMessage("중복된 아이디 입니다."); } @Test @@ -56,38 +45,10 @@ void userSaveFailureTest() { void duplicatedUsernameSaveTest() { User newUser = new User("Steve-Jobs", "helloworld"); User duplicatedUser = new User("Steve-Jobs", "helloworld"); - userSaveUseCase.save(newUser); + userSaveUseCase.save(newUser.getUsername(), newUser.getPassword()); - assertThatThrownBy(() -> userSaveUseCase.save(duplicatedUser)) + assertThatThrownBy(() -> userSaveUseCase.save(duplicatedUser.getUsername(), duplicatedUser.getPassword())) .isInstanceOf(BusinessException.class) .isExactlyInstanceOf(DuplicatedUsernameException.class); } - - @Test - @DisplayName("동시에 1_000 명의 사용자가 가입해도 PK값은 유일하다.") - void userSaveSynchronizedTest() throws InterruptedException { - int fixedUserCount = 1_000; - ExecutorService executorService = Executors.newFixedThreadPool(32); - CountDownLatch latch = new CountDownLatch(fixedUserCount); - - Set userIds = ConcurrentHashMap.newKeySet(); - for (int index = 1; index <= fixedUserCount; index++) { - User newUser = new User("Username" + index, "Password" + index); - executorService.submit(() -> { - try { - User savedUser = userSaveUseCase.save(newUser); - userIds.add(savedUser.getId()); - } finally { - latch.countDown(); - } - }); - } - - latch.await(); - - List findAllUsers = userRepository.findAll(); - log.info("findAllUsers: {}명", findAllUsers.size()); - - assertEquals(findAllUsers.size(), userIds.size()); - } } diff --git a/app/src/test/java/project/server/app/test/integrationtest/user/UserSearchIntegrationTest.java b/app/src/test/java/project/server/app/test/integrationtest/user/UserSearchIntegrationTest.java index 6a76d20..4d51288 100644 --- a/app/src/test/java/project/server/app/test/integrationtest/user/UserSearchIntegrationTest.java +++ b/app/src/test/java/project/server/app/test/integrationtest/user/UserSearchIntegrationTest.java @@ -1,13 +1,12 @@ package project.server.app.test.integrationtest.user; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertNotNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import project.server.app.core.domain.user.User; 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.service.UserService; +import project.server.app.core.web.user.application.service.UserServiceProxy; import project.server.app.core.web.user.exception.UserNotFoundException; import project.server.app.test.integrationtest.IntegrationTestBase; import static project.server.mvc.springframework.context.ApplicationContext.getBean; @@ -15,17 +14,14 @@ @DisplayName("[IntegrationTest] 사용자 조회 통합 테스트") class UserSearchIntegrationTest extends IntegrationTestBase { - private final UserSaveUseCase userSaveUseCase = getBean(UserService.class); - private final UserSearchUseCase userSearchUseCase = getBean(UserService.class); + private final UserSaveUseCase userSaveUseCase = getBean(UserServiceProxy.class); + private final UserSearchUseCase userSearchUseCase = getBean(UserServiceProxy.class); @Test @DisplayName("사용자가 저장되면 PK로 조회할 수 있다.") void userSearchTest() { User newUser = new User("Steve-Jobs", "helloworld"); - User savedUser = userSaveUseCase.save(newUser); - - User findUser = userSearchUseCase.findById(savedUser.getId()); - assertNotNull(findUser); + userSaveUseCase.save(newUser.getUsername(), newUser.getPassword()); } @Test diff --git a/app/src/test/java/project/server/app/test/unittest/user/UserUnitTest.java b/app/src/test/java/project/server/app/test/unittest/user/UserUnitTest.java index 5952b23..1bdc915 100644 --- a/app/src/test/java/project/server/app/test/unittest/user/UserUnitTest.java +++ b/app/src/test/java/project/server/app/test/unittest/user/UserUnitTest.java @@ -1,5 +1,6 @@ package project.server.app.test.unittest.user; +import java.time.LocalDateTime; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -7,6 +8,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static project.server.app.common.fixture.user.UserFixture.createUser; +import static project.server.app.core.domain.user.Deleted.FALSE; +import static project.server.app.core.domain.user.Deleted.TRUE; import project.server.app.core.domain.user.User; @DisplayName("[UnitTest] 사용자 단위 테스트") @@ -18,6 +21,34 @@ void userCreateTest() { assertNotNull(createUser()); } + @Test + @DisplayName("사용자가 최초로 생성되면 삭제 칼럼 값이 FALSE다.") + void userInitDeletedTest() { + User user = createUser(); + + assertEquals(FALSE, user.getDeleted()); + } + + @Test + @DisplayName("사용자를 삭제하면 deleted 값이 TRUE가 된다.") + void userDeleteTest() { + User user = createUser(); + LocalDateTime now = LocalDateTime.now(); + user.delete(now); + + assertEquals(TRUE, user.getDeleted()); + } + + @Test + @DisplayName("사용자를 삭제하면 최종 수정한 날짜가 현재로 바뀐다.") + void userDeleteLastModifiedAtTest() { + User user = createUser(); + LocalDateTime now = LocalDateTime.now(); + user.delete(now); + + assertEquals(now, user.getLastModifiedAt()); + } + @Test @DisplayName("equals를 재정의 했을 때, 값이 같다면 같은 객체로 인식한다.") void userEqualsTest() { 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 e9e0f32..9559bac 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 @@ -2,6 +2,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -10,6 +12,9 @@ import project.server.app.common.exception.InvalidParameterException; import project.server.app.common.exception.UnAuthorizedException; import project.server.app.core.web.user.presentation.validator.UserValidator; +import project.server.mvc.servlet.HttpServletResponse; +import project.server.mvc.servlet.Response; +import project.server.mvc.servlet.http.HttpStatus; @DisplayName("[UnitTest] 사용자 검증 단위 테스트") class UserValidatorUnitTest { @@ -42,14 +47,29 @@ void passwordNullOrBlank(String parameter) { @Test @DisplayName("사용자 아이디가 null이면 UnAuthorizedException이 발생한다.") void sessionIdNullTest() { - assertThatThrownBy(() -> validator.validateSessionId(null)) + HttpServletResponse response = new Response(); + assertThatThrownBy(() -> validator.validateSessionId(null, response)) .isInstanceOf(RuntimeException.class) .isExactlyInstanceOf(UnAuthorizedException.class); } + @Test + @DisplayName("사용자 아이디가 null이면 상태가 UnAuthorizedException이 된다.") + void sessionIdNullStatusTest() { + HttpServletResponse response = new Response(); + + assertThrows(UnAuthorizedException.class, () -> { + validator.validateSessionId(null, response); + } + ); + + assertEquals(HttpStatus.UN_AUTHORIZED, response.getStatus()); + } + @Test @DisplayName("사용자 아이디가 null이 아니라면 에러가 발생하지 않는다.") void sessionIdValidateTest() { - assertDoesNotThrow(() -> validator.validateSessionId(1L)); + HttpServletResponse response = new Response(); + assertDoesNotThrow(() -> validator.validateSessionId(1L, response)); } } diff --git a/jdbc/README.md b/jdbc/README.md new file mode 100644 index 0000000..133fb4e --- /dev/null +++ b/jdbc/README.md @@ -0,0 +1,19 @@ +## ⛺️ Jdbc 모듈 + +데이터베이스 접근 모듈. + +


+ +## 👪 패키지 간 의존관계 + +Jdbc 모듈은 다른 모듈에 의존하지 않습니다. + +| Application Module | Mvc Module | Jdbc Module | +|:------------------:|:----------:|:-----------:| +| X | X | - | + +   - Application: 애플리케이션 모듈
+   - Mvc: 스프링 Mvc 모듈
+   - Jdbc: 데이터베이스 접근 모듈
+ +
diff --git a/jdbc/build.gradle b/jdbc/build.gradle new file mode 100644 index 0000000..60edeb5 --- /dev/null +++ b/jdbc/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation("org.apache.commons:commons-dbcp2:2.10.0") +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/ConfigMap.java b/jdbc/src/main/java/project/server/jdbc/core/ConfigMap.java new file mode 100644 index 0000000..8a97de9 --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/ConfigMap.java @@ -0,0 +1,90 @@ +package project.server.jdbc.core; + +public class ConfigMap { + + private SpringConfig spring; + + public SpringConfig getSpring() { + return spring; + } + + public String getDriverClassName() { + return spring.getDriverClassName(); + } + + public String getUrl() { + return spring.getUrl(); + } + + public String getUsername() { + return spring.getUsername(); + } + + public String getPassword() { + return spring.getPassword(); + } + + static class SpringConfig { + private DatasourceConfig datasource; + + public DatasourceConfig getDatasource() { + return datasource; + } + + public String getDriverClassName() { + return datasource.getDriverClassName(); + } + + public String getUrl() { + return datasource.getUrl(); + } + + public String getUsername() { + return datasource.getUsername(); + } + + public String getPassword() { + return datasource.getPassword(); + } + + @Override + public String toString() { + return String.format("dataSource:%s", datasource); + } + + static class DatasourceConfig { + private String driverClassName; + private String url; + private String username; + private String password; + + public String getDriverClassName() { + return driverClassName; + } + + public String getUrl() { + return url; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + @Override + public String toString() { + return String.format( + "driverClassName:%s, url:%s, username:%s, password:%s", driverClassName, url, username, password + ); + } + } + } + + @Override + public String toString() { + return String.valueOf(spring); + } +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/ConnectionHolder.java b/jdbc/src/main/java/project/server/jdbc/core/ConnectionHolder.java new file mode 100644 index 0000000..54b047a --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/ConnectionHolder.java @@ -0,0 +1,43 @@ +package project.server.jdbc.core; + +import java.sql.Connection; +import java.util.Objects; + +public class ConnectionHolder { + + private String uuid; + private Connection connection; + + public Connection getConnection() { + return connection; + } + + public String getId() { + return uuid; + } + + public void setId(String uniqueId) { + this.uuid = uniqueId; + } + + public void setConnection(Connection connection) { + this.connection = connection; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + ConnectionHolder that = (ConnectionHolder) object; + return uuid.equals(that.uuid); + } + + @Override + public int hashCode() { + return Objects.hash(uuid); + } +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/DataSourceUtils.java b/jdbc/src/main/java/project/server/jdbc/core/DataSourceUtils.java new file mode 100644 index 0000000..3e823d4 --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/DataSourceUtils.java @@ -0,0 +1,38 @@ +package project.server.jdbc.core; + +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; +import lombok.extern.slf4j.Slf4j; +import static project.server.jdbc.core.transaction.TransactionSynchronizationManager.releaseResource; + +@Slf4j +public final class DataSourceUtils { + + public static void releaseConnection( + DataSource dataSource, + Connection connection + ) { + try { + doReleaseConnection(connection); + } catch (SQLException exception) { + log.error("SQLException.", exception); + } catch (Throwable exception) { + log.error("Exception.", exception); + } finally { + releaseResource(dataSource); + } + } + + public static void doReleaseConnection(Connection connection) throws SQLException { + if (connection == null) { + return; + } + doCloseConnection(connection); + } + + public static void doCloseConnection(Connection connection) throws SQLException { + connection.close(); + log.debug("Connection closed."); + } +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/DriverManager.java b/jdbc/src/main/java/project/server/jdbc/core/DriverManager.java new file mode 100644 index 0000000..f3b1746 --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/DriverManager.java @@ -0,0 +1,26 @@ +package project.server.jdbc.core; + +import javax.sql.DataSource; +import org.apache.commons.dbcp2.BasicDataSource; + +public class DriverManager { + + private final DataSource dataSource; + + public DriverManager(ConfigMap configMap) { + this.dataSource = createDataSource(configMap); + } + + private DataSource createDataSource(ConfigMap configMap) { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName(configMap.getDriverClassName()); + dataSource.setUrl(configMap.getUrl()); + dataSource.setUsername(configMap.getUsername()); + dataSource.setPassword(configMap.getPassword()); + return dataSource; + } + + public DataSource getDataSource() { + return dataSource; + } +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/exception/DataAccessException.java b/jdbc/src/main/java/project/server/jdbc/core/exception/DataAccessException.java new file mode 100644 index 0000000..fccc71a --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/exception/DataAccessException.java @@ -0,0 +1,19 @@ +package project.server.jdbc.core.exception; + +public class DataAccessException extends RuntimeException { + + private static final String INTERNAL_SERVER_ERROR = "서버 내부 오류입니다."; + + public DataAccessException() { + super(INTERNAL_SERVER_ERROR); + } + + @Override + public String toString() { + return String.format( + "{\"code\":%d, \"message\":\"%s\"}", + 500, + INTERNAL_SERVER_ERROR + ); + } +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/exception/InvalidDataAccessException.java b/jdbc/src/main/java/project/server/jdbc/core/exception/InvalidDataAccessException.java new file mode 100644 index 0000000..d72a348 --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/exception/InvalidDataAccessException.java @@ -0,0 +1,4 @@ +package project.server.jdbc.core.exception; + +public class InvalidDataAccessException extends RuntimeException { +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/ConnectionCallback.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/ConnectionCallback.java new file mode 100644 index 0000000..c12b537 --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/ConnectionCallback.java @@ -0,0 +1,9 @@ +package project.server.jdbc.core.jdbc; + +import java.sql.Connection; +import java.sql.SQLException; + +@FunctionalInterface +public interface ConnectionCallback { + T doInConnection(Connection connection) throws SQLException; +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/InitializingBean.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/InitializingBean.java new file mode 100644 index 0000000..d9bb842 --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/InitializingBean.java @@ -0,0 +1,5 @@ +package project.server.jdbc.core.jdbc; + +public interface InitializingBean { + void afterPropertiesSet() throws Exception; +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcAccessor.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcAccessor.java new file mode 100644 index 0000000..90a7495 --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcAccessor.java @@ -0,0 +1,18 @@ +package project.server.jdbc.core.jdbc; + +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; + +public abstract class JdbcAccessor implements InitializingBean { + + private DataSource dataSource; + + public JdbcAccessor(DataSource dataSource) { + this.dataSource = dataSource; + } + + public Connection getConnection() throws SQLException { + return dataSource.getConnection(); + } +} 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 new file mode 100644 index 0000000..a259bfd --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcHelper.java @@ -0,0 +1,63 @@ +package project.server.jdbc.core.jdbc; + +import java.util.Arrays; +import static java.util.stream.Collectors.joining; + +public final class JdbcHelper { + + private static final String ENGLISH_ALPHA = "([a-z])([A-Z]+)"; + private static final String REPLACEMENT_A_TO_B = "$1_$2"; + + private JdbcHelper() { + throw new AssertionError("올바른 방식으로 생성자를 호출해주세요."); + } + + public static String insert() { + return "INSERT INTO user " + + "(username, password, created_at, last_modified_at, deleted) " + + "VALUES (?, ?, ?, ?, ?)"; + } + + public static String selectBy(Class clazz) { + return "SELECT * FROM " + + convertCamelToSnake(clazz.getSimpleName()) + + " WHERE id = ? AND deleted = 'FALSE'"; + } + + public static String selectBy( + Class clazz, + String... fieldNames + ) { + String tableName = convertCamelToSnake(clazz.getSimpleName()); + String whereClause = Arrays.stream(fieldNames) + .map(fieldName -> convertCamelToSnake(fieldName) + " = ?") + .collect(joining(" AND ")); + return "SELECT * FROM " + tableName + " WHERE " + whereClause; + } + + public static String update( + Class clazz, + String fieldName + ) { + return "UPDATE " + + convertCamelToSnake(clazz.getSimpleName()) + + " SET " + + convertCamelToSnake(fieldName) + + " = 'TRUE' WHERE id = ?"; + } + + public static String selectAll(Class clazz) { + return "SELECT * FROM " + + convertCamelToSnake(clazz.getSimpleName()); + } + + public static String truncate(Class clazz) { + return "TRUNCATE TABLE " + + convertCamelToSnake(clazz.getSimpleName()); + } + + public static String convertCamelToSnake(String value) { + return value.replaceAll(ENGLISH_ALPHA, REPLACEMENT_A_TO_B) + .toLowerCase(); + } +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcOperations.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcOperations.java new file mode 100644 index 0000000..0c7c50f --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcOperations.java @@ -0,0 +1,15 @@ +package project.server.jdbc.core.jdbc; + +import java.util.List; + +public interface JdbcOperations { + T queryForObject(ConnectionCallback action); + + T queryForObject(String sql, PreparedStatementCallback action); + + T queryForObject(String sql, PreparedStatementSetter pss, ResultSetExtractor rse); + + List queryForList(String sql, RowMapper rowMapper, Object... params); + + void execute(String sql); +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcTemplate.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcTemplate.java new file mode 100644 index 0000000..13458e2 --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/JdbcTemplate.java @@ -0,0 +1,111 @@ +package project.server.jdbc.core.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import javax.sql.DataSource; +import lombok.extern.slf4j.Slf4j; +import project.server.jdbc.core.exception.DataAccessException; + +@Slf4j +public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { + + public JdbcTemplate(DataSource dataSource) { + super(dataSource); + } + + @Override + public T queryForObject(ConnectionCallback action) { + try (Connection conn = getConnection()) { + return action.doInConnection(conn); + } catch (SQLException exception) { + log.error("SQLException: {}", exception.getMessage()); + throw new DataAccessException(); + } + } + + @Override + public T queryForObject( + String sql, + PreparedStatementCallback action + ) { + return queryForObject(conn -> { + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + return action.doInPreparedStatement(pstmt); + } catch (SQLException exception) { + log.error("SQLException: {}", exception.getMessage()); + throw new DataAccessException(); + } + }); + } + + @Override + public T queryForObject( + String sql, + PreparedStatementSetter pss, + ResultSetExtractor rse + ) { + return queryForObject(sql, pstmt -> { + pss.setValues(pstmt); + try (ResultSet rs = pstmt.executeQuery()) { + return rse.extractData(rs); + } catch (SQLException exception) { + log.error("SQLException: {}", exception.getMessage()); + throw new DataAccessException(); + } + }); + } + + @Override + public List queryForList( + String sql, + RowMapper rowMapper, + Object... params + ) { + return queryForObject( + sql, pstmt -> + setParameters(pstmt, params), rs -> { + List results = new ArrayList<>(); + while (rs.next()) { + results.add(rowMapper.mapRow(rs)); + } + return results; + } + ); + } + + @Override + public void execute(String sql) { + try ( + Connection conn = getConnection(); + PreparedStatement pstmt = conn.prepareStatement(sql) + ) { + pstmt.executeUpdate(); + } catch (SQLException exception) { + log.error("SQLException: {}", exception.getMessage()); + throw new DataAccessException(); + } + } + + private void setParameters( + PreparedStatement pstmt, + Object... params + ) { + try { + for (int index = 0; index < params.length; index++) { + pstmt.setObject(index + 1, params[index]); + } + } catch (SQLException exception) { + log.error("SQLException: {}", exception.getMessage()); + throw new DataAccessException(); + } + } + + @Override + public void afterPropertiesSet() { + log.info("AfterProperties."); + } +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/PreparedStatementCallback.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/PreparedStatementCallback.java new file mode 100644 index 0000000..64a5d1f --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/PreparedStatementCallback.java @@ -0,0 +1,9 @@ +package project.server.jdbc.core.jdbc; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@FunctionalInterface +public interface PreparedStatementCallback { + T doInPreparedStatement(PreparedStatement pstmt) throws SQLException; +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/PreparedStatementSetter.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/PreparedStatementSetter.java new file mode 100644 index 0000000..78ad86a --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/PreparedStatementSetter.java @@ -0,0 +1,9 @@ +package project.server.jdbc.core.jdbc; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@FunctionalInterface +public interface PreparedStatementSetter { + void setValues(PreparedStatement pstmt) throws SQLException; +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/ResultSetExtractor.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/ResultSetExtractor.java new file mode 100644 index 0000000..332384c --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/ResultSetExtractor.java @@ -0,0 +1,9 @@ +package project.server.jdbc.core.jdbc; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@FunctionalInterface +public interface ResultSetExtractor { + T extractData(ResultSet resultSet) throws SQLException; +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/jdbc/RowMapper.java b/jdbc/src/main/java/project/server/jdbc/core/jdbc/RowMapper.java new file mode 100644 index 0000000..82892ae --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/jdbc/RowMapper.java @@ -0,0 +1,9 @@ +package project.server.jdbc.core.jdbc; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@FunctionalInterface +public interface RowMapper { + T mapRow(ResultSet rs) throws SQLException; +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/AbstractPlatformTransactionManager.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/AbstractPlatformTransactionManager.java new file mode 100644 index 0000000..4a28793 --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/AbstractPlatformTransactionManager.java @@ -0,0 +1,24 @@ +package project.server.jdbc.core.transaction; + +import java.sql.SQLException; + +public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager { + + @Override + public final TransactionStatus getTransaction(TransactionDefinition definition) throws SQLException { + Object transaction = doGetTransaction(); + return startTransaction(definition, transaction); + } + + private TransactionStatus startTransaction( + TransactionDefinition definition, + Object transaction + ) throws SQLException { + doBegin(transaction, definition); + return new DefaultTransactionStatus(transaction, true, false); + } + + abstract void doBegin(Object transaction, TransactionDefinition definition) throws SQLException; + + protected abstract Object doGetTransaction(); +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/AbstractTransactionStatus.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/AbstractTransactionStatus.java new file mode 100644 index 0000000..a316c9b --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/AbstractTransactionStatus.java @@ -0,0 +1,6 @@ +package project.server.jdbc.core.transaction; + +import project.server.jdbc.core.transaction.TransactionStatus; + +public abstract class AbstractTransactionStatus implements TransactionStatus { +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/DataSourceTransactionManager.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/DataSourceTransactionManager.java new file mode 100644 index 0000000..63b7b5e --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/DataSourceTransactionManager.java @@ -0,0 +1,79 @@ +package project.server.jdbc.core.transaction; + +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; +import lombok.extern.slf4j.Slf4j; +import static project.server.jdbc.core.DataSourceUtils.releaseConnection; +import project.server.jdbc.core.exception.DataAccessException; +import static project.server.jdbc.core.transaction.TransactionSynchronizationManager.bindResource; + +@Slf4j +public class DataSourceTransactionManager extends AbstractPlatformTransactionManager { + + private final DataSource dataSource; + + public DataSourceTransactionManager(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Override + void doBegin( + Object transaction, + TransactionDefinition definition + ) throws SQLException { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + Connection connection = dataSource.getConnection(); + log.info("Get connection."); + + txObject.setConnection(definition.getId(), connection); + connection.setAutoCommit(false); + log.info("Set auto-commit false."); + prepareConnectionForTransaction(connection, definition); + + bindResource(dataSource, txObject.getConnectionHolder()); + } + + private void prepareConnectionForTransaction( + Connection connection, + TransactionDefinition definition + ) throws SQLException { + if (definition != null && definition.isReadOnly()) { + connection.setReadOnly(true); + } + } + + @Override + public void commit(TransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + Connection connection = txObject.getConnection(); + + try { + connection.commit(); + } catch (Exception exception) { + throw new DataAccessException(); + } finally { + releaseConnection(dataSource, connection); + } + } + + @Override + public void rollback(TransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + Connection connection = txObject.getConnection(); + + try { + connection.rollback(); + } catch (Exception exception) { + throw new RuntimeException(); + } finally { + releaseConnection(dataSource, connection); + log.debug("Rollback, transaction failed."); + } + } + + @Override + protected Object doGetTransaction() { + return new DataSourceTransactionObject(); + } +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/DataSourceTransactionObject.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/DataSourceTransactionObject.java new file mode 100644 index 0000000..50bd498 --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/DataSourceTransactionObject.java @@ -0,0 +1,38 @@ +package project.server.jdbc.core.transaction; + +import java.sql.Connection; +import project.server.jdbc.core.ConnectionHolder; + +public class DataSourceTransactionObject { + + private final ConnectionHolder connectionHolder; + + public DataSourceTransactionObject() { + this.connectionHolder = new ConnectionHolder(); + } + + public String getId() { + return connectionHolder.getId(); + } + + public Connection getConnection() { + return connectionHolder.getConnection(); + } + + public ConnectionHolder getConnectionHolder() { + return connectionHolder; + } + + public void setConnection( + String uniqueId, + Connection connection + ) { + this.connectionHolder.setId(uniqueId); + this.connectionHolder.setConnection(connection); + } + + @Override + public String toString() { + return String.format("id: %s", connectionHolder.getId()); + } +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/DefaultTransactionDefinition.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/DefaultTransactionDefinition.java new file mode 100644 index 0000000..c7537f5 --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/DefaultTransactionDefinition.java @@ -0,0 +1,28 @@ +package project.server.jdbc.core.transaction; + +import static java.util.UUID.randomUUID; + +public class DefaultTransactionDefinition implements TransactionDefinition { + + private final String id; + private final boolean readOnly; + + public DefaultTransactionDefinition() { + this.id = randomUUID().toString(); + this.readOnly = false; + } + + public DefaultTransactionDefinition(boolean readOnly) { + this.id = randomUUID().toString(); + this.readOnly = readOnly; + } + + public static DefaultTransactionDefinition createTransactionDefinition(boolean readOnly) { + return new DefaultTransactionDefinition(readOnly); + } + + @Override + public String getId() { + return id; + } +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/DefaultTransactionStatus.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/DefaultTransactionStatus.java new file mode 100644 index 0000000..af090fe --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/DefaultTransactionStatus.java @@ -0,0 +1,28 @@ +package project.server.jdbc.core.transaction; + +public class DefaultTransactionStatus extends AbstractTransactionStatus { + + private final Object transaction; + private final boolean newTransaction; + private final boolean readOnly; + + public DefaultTransactionStatus( + Object transaction, + boolean newTransaction, + boolean readOnly + ) { + this.transaction = transaction; + this.newTransaction = newTransaction; + this.readOnly = readOnly; + } + + @Override + public Object getTransaction() { + return transaction; + } + + @Override + public String toString() { + return String.format("%s, newTransaction:%s, readOnly:%s", transaction, newTransaction, readOnly); + } +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/JdbcTransactionManager.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/JdbcTransactionManager.java new file mode 100644 index 0000000..58b4db0 --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/JdbcTransactionManager.java @@ -0,0 +1,10 @@ +package project.server.jdbc.core.transaction; + +import javax.sql.DataSource; + +public class JdbcTransactionManager extends DataSourceTransactionManager { + + public JdbcTransactionManager(DataSource dataSource) { + super(dataSource); + } +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/PlatformTransactionManager.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/PlatformTransactionManager.java new file mode 100644 index 0000000..1911fcd --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/PlatformTransactionManager.java @@ -0,0 +1,11 @@ +package project.server.jdbc.core.transaction; + +import java.sql.SQLException; + +public interface PlatformTransactionManager extends TransactionManager { + TransactionStatus getTransaction(TransactionDefinition definition) throws SQLException; + + void commit(TransactionStatus status); + + void rollback(TransactionStatus status); +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionDefinition.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionDefinition.java new file mode 100644 index 0000000..c8bf64e --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionDefinition.java @@ -0,0 +1,9 @@ +package project.server.jdbc.core.transaction; + +public interface TransactionDefinition { + String getId(); + + default boolean isReadOnly() { + return false; + } +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionManager.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionManager.java new file mode 100644 index 0000000..b3fcc94 --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionManager.java @@ -0,0 +1,4 @@ +package project.server.jdbc.core.transaction; + +public interface TransactionManager { +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionStatus.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionStatus.java new file mode 100644 index 0000000..26808d2 --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionStatus.java @@ -0,0 +1,5 @@ +package project.server.jdbc.core.transaction; + +public interface TransactionStatus { + Object getTransaction(); +} diff --git a/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionSynchronizationManager.java b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionSynchronizationManager.java new file mode 100644 index 0000000..d7f28a3 --- /dev/null +++ b/jdbc/src/main/java/project/server/jdbc/core/transaction/TransactionSynchronizationManager.java @@ -0,0 +1,38 @@ +package project.server.jdbc.core.transaction; + +import java.util.HashMap; +import java.util.Map; +import javax.sql.DataSource; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public final class TransactionSynchronizationManager { + + private static final ThreadLocal> factory = new ThreadLocal<>(); + + private TransactionSynchronizationManager() { + throw new AssertionError("올바른 방식으로 생성자를 호출해주세요."); + } + + public static void bindResource( + DataSource key, + Object value + ) { + Map transactionMap = getTransactionMap(); + transactionMap.put(key, value); + log.debug("Put {} into threadlocal.", key); + } + + private static Map getTransactionMap() { + if (factory.get() == null) { + factory.set(new HashMap<>()); + } + return factory.get(); + } + + public static void releaseResource(DataSource key) { + Map transactionMap = factory.get(); + transactionMap.remove(key); + log.debug("Remove {} from threadlocal.", key); + } +} diff --git a/mvc/README.md b/mvc/README.md new file mode 100644 index 0000000..29c4355 --- /dev/null +++ b/mvc/README.md @@ -0,0 +1,19 @@ +## ⛺️ Mvc 모듈 + +스프링 Mvc 모듈. + +


+ +## 👪 패키지 간 의존관계 + +Mvc 모듈은 다른 모듈에 의존하지 않습니다. + +| Application Module | Mvc Module | Jdbc Module | +|:------------------:|:----------:|:-----------:| +| X | - | X | + +   - Application: 애플리케이션 모듈
+   - Mvc: 스프링 Mvc 모듈
+   - Jdbc: 데이터베이스 접근 모듈
+ +
diff --git a/mvc/build.gradle b/mvc/build.gradle index 5e29569..34a1707 100644 --- a/mvc/build.gradle +++ b/mvc/build.gradle @@ -1,3 +1,4 @@ dependencies { implementation("org.reflections:reflections:${reflectionsVersion}") + implementation("com.fasterxml.jackson.core:jackson-databind:2.16.1") } diff --git a/mvc/src/main/java/project/server/mvc/Acceptor.java b/mvc/src/main/java/project/server/mvc/Acceptor.java index 1ce1a67..79b8a37 100644 --- a/mvc/src/main/java/project/server/mvc/Acceptor.java +++ b/mvc/src/main/java/project/server/mvc/Acceptor.java @@ -3,25 +3,33 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; +import static java.nio.ByteBuffer.allocate; import java.nio.channels.SelectionKey; +import static java.nio.channels.SelectionKey.OP_READ; +import static java.nio.channels.SelectionKey.OP_WRITE; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Set; -import lombok.SneakyThrows; +import java.util.concurrent.ExecutorService; +import static java.util.concurrent.Executors.newFixedThreadPool; import lombok.extern.slf4j.Slf4j; -import project.server.mvc.tomcat.AsyncRequest; +import project.server.mvc.servlet.HttpServletResponse; +import static project.server.mvc.servlet.http.HttpStatus.INTERNAL_SERVER_ERROR; +import project.server.mvc.tomcat.AbstractEndpoint; import project.server.mvc.tomcat.Nio2EndPoint; +import project.server.mvc.tomcat.NioSocketWrapper; @Slf4j public class Acceptor implements Runnable { - private static final int FINISHED = -1; + private static final int FIXED_THREAD_COUNT = 32; private static final int BUFFER_CAPACITY = 1024; private final Selector selector; - private final Nio2EndPoint nio2EndPoint; + private final AbstractEndpoint nio2EndPoint; + private final ExecutorService service = newFixedThreadPool(FIXED_THREAD_COUNT); public Acceptor( int port, @@ -40,22 +48,25 @@ private void initContext(int port) throws IOException { } @Override - @SneakyThrows public void run() { while (true) { - selector.select(); - Set selectedKeys = selector.selectedKeys(); - Iterator keys = selectedKeys.iterator(); - - while (keys.hasNext()) { - SelectionKey key = keys.next(); - - if (key.isAcceptable()) { - acceptSocket(key, selector); - } else if (key.isReadable()) { - read(key); + try { + selector.select(); + Set selectedKeys = selector.selectedKeys(); + Iterator keys = selectedKeys.iterator(); + while (keys.hasNext()) { + SelectionKey key = keys.next(); + if (key.isAcceptable()) { + acceptSocket(key, selector); + } else if (key.isReadable()) { + read(key); + } else if (key.isWritable()) { + write(key); + } + keys.remove(); } - keys.remove(); + } catch (IOException exception) { + throw new RuntimeException(exception); } } } @@ -63,25 +74,48 @@ public void run() { private void acceptSocket( SelectionKey key, Selector selector - ) throws IOException { + ) { ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); - SocketChannel socketChannel = serverChannel.accept(); - socketChannel.configureBlocking(false); - socketChannel.register(selector, SelectionKey.OP_READ); + try { + SocketChannel socketChannel = serverChannel.accept(); + socketChannel.configureBlocking(false); + socketChannel.register(selector, OP_READ); + } catch (IOException exception) { + throw new RuntimeException(exception); + } } - private void read(SelectionKey key) throws Exception { - SocketChannel socketChannel = (SocketChannel) key.channel(); - ByteBuffer buffer = ByteBuffer.allocate(BUFFER_CAPACITY); - int bytesRead = socketChannel.read(buffer); - if (bytesRead == FINISHED) { - socketChannel.close(); - log.info("Connection closed by client."); - return; + private void read(SelectionKey key) { + log.debug("READ"); + NioSocketWrapper socketWrapper = null; + try { + SocketChannel socketChannel = (SocketChannel) key.channel(); + ByteBuffer buffer = allocate(BUFFER_CAPACITY); + socketWrapper = new NioSocketWrapper(socketChannel, buffer); + socketWrapper.flip(); + + socketChannel.register(selector, OP_WRITE); + key.attach(socketWrapper); + } catch (IOException exception) { + if (socketWrapper != null) { + HttpServletResponse response = socketWrapper.getResponse(); + response.setStatus(INTERNAL_SERVER_ERROR); + } } + } - buffer.flip(); - new AsyncRequest(socketChannel, buffer) - .run(); + private void write(SelectionKey key) { + log.debug("WRITE"); + SocketChannel socketChannel = (SocketChannel) key.channel(); + service.submit((Runnable) key.attachment()); + try { + socketChannel.register(selector, OP_READ); + } catch (IOException exception) { + Object object = key.attachment(); + if (object != null) { + HttpServletResponse response = (HttpServletResponse) object; + response.setStatus(INTERNAL_SERVER_ERROR); + } + } } } diff --git a/mvc/src/main/java/project/server/mvc/servlet/HttpServletResponse.java b/mvc/src/main/java/project/server/mvc/servlet/HttpServletResponse.java index 44a42d3..996542a 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/HttpServletResponse.java +++ b/mvc/src/main/java/project/server/mvc/servlet/HttpServletResponse.java @@ -1,19 +1,24 @@ package project.server.mvc.servlet; +import java.io.IOException; import java.nio.channels.SocketChannel; import project.server.mvc.servlet.http.Cookie; import project.server.mvc.servlet.http.HttpStatus; public interface HttpServletResponse extends ServletResponse { - String getStatusAsString(); + String getHttpHeaderLine(); + + HttpStatus getStatus(); void setStatus(HttpStatus status); void addCookie(Cookie cookie); - String getCookiesAsString(); + void setHeader(String key, String value); - SocketChannel getSocketChannel(); + void setBody(String body); - HttpStatus getStatus(); + void write(String data) throws IOException; + + void write(byte[] data) throws IOException; } 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 a0628e8..161006b 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/Request.java +++ b/mvc/src/main/java/project/server/mvc/servlet/Request.java @@ -119,6 +119,6 @@ public String getHeader(String key) { @Override public String toString() { - return String.format("%s%s\r\n%s", requestLine, headers, requestBody); + return String.format("%s%s%s", requestLine, headers, requestBody); } } diff --git a/mvc/src/main/java/project/server/mvc/servlet/Response.java b/mvc/src/main/java/project/server/mvc/servlet/Response.java index 498aa00..e10fa85 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/Response.java +++ b/mvc/src/main/java/project/server/mvc/servlet/Response.java @@ -1,46 +1,49 @@ package project.server.mvc.servlet; -import java.io.OutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; +import static java.nio.charset.StandardCharsets.UTF_8; import project.server.mvc.servlet.http.Cookie; import project.server.mvc.servlet.http.HttpHeaders; import project.server.mvc.servlet.http.HttpStatus; -import static project.server.mvc.servlet.http.HttpStatus.OK; +import project.server.mvc.servlet.http.ResponseBody; +import project.server.mvc.servlet.http.StatusLine; public class Response implements HttpServletResponse { - private HttpStatus status; + private final StatusLine statusLine; private final HttpHeaders headers; + private final ResponseBody responseBody; private final SocketChannel socketChannel; - private final OutputStream outputStream; - public Response( - SocketChannel socketChannel, - OutputStream outputStream - ) { - this.socketChannel = socketChannel; - this.status = OK; + public Response() { + this.statusLine = new StatusLine(); + this.socketChannel = null; this.headers = new HttpHeaders(); - this.outputStream = outputStream; + this.responseBody = new ResponseBody(); } - public Response(OutputStream outputStream) { - this(null, outputStream); + public Response(SocketChannel socketChannel) { + this.statusLine = new StatusLine(); + this.socketChannel = socketChannel; + this.headers = new HttpHeaders(); + this.responseBody = new ResponseBody(); } @Override - public OutputStream getOutputStream() { - return outputStream; + public String getHttpHeaderLine() { + return String.format("%s%s", statusLine, headers); } @Override - public String getStatusAsString() { - return status.getStatus(); + public HttpStatus getStatus() { + return statusLine.getHttpStatus(); } @Override public void setStatus(HttpStatus status) { - this.status = status; + statusLine.setStatus(status); } @Override @@ -49,17 +52,36 @@ public void addCookie(Cookie cookie) { } @Override - public String getCookiesAsString() { - return headers.getCookiesAsString(); + public void setHeader( + String key, + String value + ) { + headers.addHeader(key, value); } @Override - public SocketChannel getSocketChannel() { - return socketChannel; + public void setBody(String body) { + this.responseBody.setBody(body); } @Override - public HttpStatus getStatus() { - return status; + public void write(String data) throws IOException { + ByteBuffer headerBuffer = ByteBuffer.wrap(data.getBytes(UTF_8)); + while (headerBuffer.hasRemaining()) { + socketChannel.write(headerBuffer); + } + } + + @Override + public void write(byte[] data) throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(data); + while (buffer.hasRemaining()) { + socketChannel.write(buffer); + } + } + + @Override + public String toString() { + return String.format("%s%s%s", statusLine, headers, responseBody); } } diff --git a/mvc/src/main/java/project/server/mvc/servlet/ServletResponse.java b/mvc/src/main/java/project/server/mvc/servlet/ServletResponse.java index 370e3ed..7d89ae9 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/ServletResponse.java +++ b/mvc/src/main/java/project/server/mvc/servlet/ServletResponse.java @@ -1,8 +1,4 @@ package project.server.mvc.servlet; -import java.io.IOException; -import java.io.OutputStream; - public interface ServletResponse { - OutputStream getOutputStream() throws IOException; } diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/Cookies.java b/mvc/src/main/java/project/server/mvc/servlet/http/Cookies.java index 9ec3ea8..d4ffa7c 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/http/Cookies.java +++ b/mvc/src/main/java/project/server/mvc/servlet/http/Cookies.java @@ -6,11 +6,13 @@ public class Cookies { + private static final String EMPTY_STRING = ""; private static final int KEY = 0; private static final int VALUE = 1; private static final String DELIMITER = "="; private static final String COOKIE_DELIMITER = "; "; - private static final String CARRIAGE_RETURN = "\r\n"; + private static final String SET_COOKIE_DELIMITER = ": "; + private static final String SET_COOKIE = "Set-Cookie"; private final Map cookiesMap; public static final Cookies emptyCookies = new Cookies(); @@ -56,15 +58,20 @@ public void add(Cookie cookie) { @Override public String toString() { + if (cookiesMap.isEmpty()) { + return EMPTY_STRING; + } + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(SET_COOKIE) + .append(SET_COOKIE_DELIMITER); + for (String key : cookiesMap.keySet()) { - if (!stringBuilder.isEmpty()) { - stringBuilder.append(COOKIE_DELIMITER); - } stringBuilder.append(key) .append(DELIMITER) - .append(cookiesMap.get(key).value()); + .append(cookiesMap.get(key).value()) + .append(COOKIE_DELIMITER); } - return stringBuilder.toString().trim() + CARRIAGE_RETURN; + return stringBuilder.toString().trim(); } } 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 def2c27..019a0c2 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 @@ -2,18 +2,18 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import static java.util.Collections.emptyList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import static java.util.stream.Collectors.joining; public class HttpHeaders { private static final int KEY = 0; private static final int VALUE = 1; private static final String HEADER_DELIMITER = ": "; + private static final String HEADER_JOINING_DELIMITER = ", "; private static final String MULTI_VALUE_DELIMITER = ","; private static final String CARRIAGE_RETURN = "\r\n"; private static final String COOKIE = "Cookie"; @@ -94,38 +94,38 @@ public void addCookie(Cookie cookie) { this.cookies.add(cookie); } - public String getCookiesAsString() { - return cookies.toString(); - } - public Cookies getCookies() { return cookies; } + public void addHeader( + String key, + String value + ) { + List values = this.headers + .getOrDefault(key, new ArrayList<>()); + values.add(new HttpHeader(key, value)); + this.headers.put(key, values); + } + @Override public String toString() { - StringBuilder stringBuilder = new StringBuilder(); - List keys = new ArrayList<>(this.headers.keySet()); - Collections.sort(keys); - + StringBuilder stringBuilder = new StringBuilder(); for (String key : keys) { - if (COOKIE.equals(key)) { - stringBuilder.append(key) - .append(HEADER_DELIMITER) - .append(cookies); - continue; - } stringBuilder.append(key) .append(HEADER_DELIMITER) .append(joiningValues(headers.get(key))); } + + stringBuilder.append(cookies) + .append(CARRIAGE_RETURN); return stringBuilder.toString(); } private String joiningValues(List headers) { return headers.stream() .map(HttpHeader::getValue) - .collect(Collectors.joining(", ")) + CARRIAGE_RETURN; + .collect(joining(HEADER_JOINING_DELIMITER)) + CARRIAGE_RETURN; } } diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/HttpStatus.java b/mvc/src/main/java/project/server/mvc/servlet/http/HttpStatus.java index 50c4500..7f5e42c 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/http/HttpStatus.java +++ b/mvc/src/main/java/project/server/mvc/servlet/http/HttpStatus.java @@ -1,11 +1,14 @@ package project.server.mvc.servlet.http; +import java.util.Arrays; +import java.util.function.Predicate; + public enum HttpStatus { OK("200 OK", 200), NO_CONTENT("204 No Content", 204), MOVE_PERMANENTLY("301 Moved Permanently", 301), BAD_REQUEST("400 Bad Request", 400), - NOT_FOUND("NOT_FOUND", 404), + NOT_FOUND("404 NOT_FOUND", 404), UN_AUTHORIZED("401 Unauthorized", 401), INTERNAL_SERVER_ERROR("500 Internal Server Error", 500); @@ -20,6 +23,17 @@ public enum HttpStatus { this.statusCode = statusCode; } + public static HttpStatus findByCode(int code) { + return Arrays.stream(values()) + .filter(equals(code)) + .findAny() + .orElseThrow(IllegalStateException::new); + } + + private static Predicate equals(int code) { + return statusCode -> statusCode.getStatusCode() == code; + } + public String getStatus() { return status; } 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 9d938c2..9bb79a7 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 @@ -5,6 +5,8 @@ public class RequestBody { + private static final String CARRIAGE_RETURN = "\r\n"; + private static final String EMPTY_STRING = ""; private static final String VALUE_DELIMITER = "="; private static final String VALUES_DELIMITER = "&"; private static final int KEY = 0; @@ -36,6 +38,6 @@ public String getAttribute(String key) { @Override public String toString() { - return String.format("%s", attributes); + return String.format("%s", attributes == null ? EMPTY_STRING + CARRIAGE_RETURN : attributes + CARRIAGE_RETURN); } } diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/ResponseBody.java b/mvc/src/main/java/project/server/mvc/servlet/http/ResponseBody.java index 384b27d..6647588 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/http/ResponseBody.java +++ b/mvc/src/main/java/project/server/mvc/servlet/http/ResponseBody.java @@ -5,20 +5,22 @@ public class ResponseBody { private static final String EMPTY_BODY = ""; + private static final String CARRIAGE_RETURN = "\r\n"; + private String body; public ResponseBody() { this.body = EMPTY_BODY; } - public ResponseBody(String body) { - this.body = body; - } - public String getBody() { return body; } + public void setBody(String body) { + this.body = body; + } + @Override public boolean equals(Object object) { if (this == object) { @@ -38,6 +40,9 @@ public int hashCode() { @Override public String toString() { - return body; + if (body.isBlank()) { + return ""; + } + return body + CARRIAGE_RETURN; } } diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/StatusLine.java b/mvc/src/main/java/project/server/mvc/servlet/http/StatusLine.java index 8fa67a3..461839e 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/http/StatusLine.java +++ b/mvc/src/main/java/project/server/mvc/servlet/http/StatusLine.java @@ -2,6 +2,7 @@ public class StatusLine { + private static final String CARRIAGE_RETURN = "\r\n"; private static final HttpVersion basicProtocolVersion = HttpVersion.HTTP_1_1; private String protocolVersion; @@ -9,6 +10,7 @@ public class StatusLine { public StatusLine() { this.protocolVersion = basicProtocolVersion.getValue(); + this.httpStatus = HttpStatus.OK; } public StatusLine(HttpStatus httpStatus) { @@ -23,11 +25,16 @@ public HttpStatus getHttpStatus() { return httpStatus; } + public String getHttpStatusAsString() { + return httpStatus.getStatus(); + } + public void setStatus(HttpStatus status) { this.httpStatus = status; } - public String getHttpStatusAsString() { - return httpStatus.getStatus(); + @Override + public String toString() { + return String.format("%s %s", protocolVersion, httpStatus.getStatus()) + CARRIAGE_RETURN; } } 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 696106d..43ff168 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 @@ -3,23 +3,32 @@ 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.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import org.reflections.Reflections; -import org.reflections.scanners.SubTypesScanner; -import org.reflections.scanners.TypeAnnotationsScanner; -import org.reflections.util.ClasspathHelper; +import static org.reflections.scanners.Scanners.SubTypes; +import static org.reflections.scanners.Scanners.TypesAnnotated; +import static org.reflections.util.ClasspathHelper.forPackage; import org.reflections.util.ConfigurationBuilder; import project.server.mvc.springframework.annotation.Bean; import project.server.mvc.springframework.annotation.Component; import project.server.mvc.springframework.annotation.Configuration; public class ApplicationContext { - private static final Map, Object> beans = new HashMap<>(); - private static final Map, Object> dependenciesInjectedBeans = new HashMap<>(); - private static final Map dependenciesInjectedBeansByName = new HashMap<>(); + + private static final int FIRST_CONSTRUCTOR = 0; + private static final String PROXY = "Proxy"; + private static final String DATASOURCE = "Datasource"; + + private static final Set> allBeans = new HashSet<>(); + private static final Map nameKeyBeans = new HashMap<>(); + private static final Map, Object> clazzKeyBeans = new HashMap<>(); + private static final Map, Object> dependencyInjectedBeans = new HashMap<>(); private static Reflections reflections; @@ -32,6 +41,14 @@ public ApplicationContext(String... packages) throws Exception { processConfigurations(configurations); } + private Reflections getReflections(String... packages) { + ConfigurationBuilder configurationBuilder = createConfigurationBuilder(); + for (String packageName : packages) { + configurationBuilder.addUrls(forPackage(packageName)); + } + return new Reflections(configurationBuilder); + } + private void componentScan(Set> components) throws Exception { for (Class component : components) { if (isInstance(component)) { @@ -41,7 +58,7 @@ private void componentScan(Set> components) throws Exception { for (Class instance : components) { if (isInstance(instance)) { - injectDependencies(instance); + put(instance); registerNamedInstance(instance); } } @@ -59,99 +76,126 @@ private void processConfigurations(Set> configurations) throws Exceptio } } + private boolean isInstance(Class clazz) { + return !clazz.isAnnotation() + && !clazz.isInterface() + && !Modifier.isAbstract(clazz.getModifiers()); + } + private Object createInstance(Class clazz) throws Exception { Constructor constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); return constructor.newInstance(); } - private void processBeanMethod(Object configInstance, Method method) throws Exception { + private void processBeanMethod( + Object configInstance, + Method method + ) throws Exception { Class returnType = method.getReturnType(); - if (!Modifier.isStatic(method.getModifiers()) && returnType != void.class) { + if (!isStatic(method.getModifiers()) && isNotVoid(returnType)) { Object bean = method.invoke(configInstance); - beans.put(returnType, bean); - dependenciesInjectedBeansByName.put(method.getName(), bean); + clazzKeyBeans.put(returnType, bean); + nameKeyBeans.put(method.getName(), bean); } } - private Reflections getReflections(String... packages) { - ConfigurationBuilder configurationBuilder = new ConfigurationBuilder() - .setScanners(new SubTypesScanner(), new TypeAnnotationsScanner()); - for (String packageName : packages) { - configurationBuilder.addUrls(ClasspathHelper.forPackage(packageName)); - } - return new Reflections(configurationBuilder); + private boolean isNotVoid(Class returnType) { + return returnType != void.class; } - private boolean isInstance(Class clazz) { - return !clazz.isAnnotation() && !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()); + private ConfigurationBuilder createConfigurationBuilder() { + return new ConfigurationBuilder() + .setScanners(SubTypes, TypesAnnotated); } private void registerNamedInstance(Class clazz) { - Object instance = dependenciesInjectedBeans.get(clazz); + Object instance = dependencyInjectedBeans.get(clazz); if (instance != null) { - dependenciesInjectedBeansByName.put(clazz.getSimpleName(), instance); + nameKeyBeans.put(clazz.getSimpleName(), instance); } } public static T getBean(String beanName) { @SuppressWarnings("unchecked") - T bean = (T) dependenciesInjectedBeansByName.get(beanName); + T bean = (T) ApplicationContext.nameKeyBeans.get(beanName); return bean; } private void add(Class clazz) throws Exception { - if (beans.containsKey(clazz)) { + if (clazzKeyBeans.containsKey(clazz) || allBeans.contains(clazz)) { return; } + allBeans.add(clazz); if (clazz.isInterface()) { + if (isDataSource(clazz)) { + return; + } + @SuppressWarnings("unchecked") Set> implementations = reflections.getSubTypesOf((Class) clazz); if (implementations.isEmpty()) { throw new IllegalStateException("No implementation found for interface: " + clazz.getName()); } - Class subTypeClass = implementations.iterator().next(); - add(subTypeClass); - beans.put(clazz, beans.get(subTypeClass)); + Class concreteClass = implementations.stream() + .filter(containsName(PROXY)) + .findAny() + .orElse(implementations.iterator().next()); + + add(concreteClass); + clazzKeyBeans.put(clazz, clazzKeyBeans.get(concreteClass)); return; } - Constructor constructor = clazz.getDeclaredConstructors()[0]; + Object instance = injectDependency(clazz); + clazzKeyBeans.put(clazz, instance); + dependencyInjectedBeans.put(clazz, instance); + } + + private boolean isDataSource(Class clazz) { + return DATASOURCE.equals(clazz.getSimpleName()); + } + + private Object injectDependency(Class clazz) throws Exception { + Constructor constructor = clazz.getDeclaredConstructors()[FIRST_CONSTRUCTOR]; constructor.setAccessible(true); Class[] paramTypes = constructor.getParameterTypes(); Object[] params = new Object[paramTypes.length]; for (int index = 0; index < paramTypes.length; index++) { Class parameterType = paramTypes[index]; - if (!beans.containsKey(parameterType)) { + if (!clazzKeyBeans.containsKey(parameterType)) { add(parameterType); } - params[index] = beans.get(parameterType); + params[index] = clazzKeyBeans.get(parameterType); } - Object instance = constructor.newInstance(params); - beans.put(clazz, instance); + return constructor.newInstance(params); + } + + private Predicate> containsName(String name) { + return clazz -> clazz.getSimpleName().contains(name); } - private void injectDependencies(Class clazz) { - if (dependenciesInjectedBeans.containsKey(clazz)) { + private void put(Class clazz) { + if (dependencyInjectedBeans.containsKey(clazz)) { return; } - Object instance = beans.get(clazz); + Object instance = clazzKeyBeans.get(clazz); if (instance == null) { throw new IllegalStateException("Instance not found: " + clazz.getName()); } - dependenciesInjectedBeans.put(clazz, instance); + dependencyInjectedBeans.put(clazz, instance); } public static T getBean(Class clazz) { - return clazz.cast(dependenciesInjectedBeans.get(clazz)); + return clazz.cast(dependencyInjectedBeans.get(clazz)); } public static Collection getAllDependencyInjectedInstances() { - return dependenciesInjectedBeansByName.values(); + return nameKeyBeans.values(); } } diff --git a/mvc/src/main/java/project/server/mvc/springframework/exception/ErrorResponse.java b/mvc/src/main/java/project/server/mvc/springframework/exception/ErrorResponse.java new file mode 100644 index 0000000..d4e3497 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/springframework/exception/ErrorResponse.java @@ -0,0 +1,39 @@ +package project.server.mvc.springframework.exception; + +public class ErrorResponse { + + private int code; + private String message; + + private ErrorResponse() { + } + + public ErrorResponse( + int code, + String message + ) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } + + public void setCode(int code) { + this.code = code; + } + + public void setMessage(String message) { + this.message = message; + } + + @Override + public String toString() { + return String.format("code:%s, message:%s", code, message); + } +} diff --git a/mvc/src/main/java/project/server/mvc/springframework/handler/RequestHandler.java b/mvc/src/main/java/project/server/mvc/springframework/handler/RequestHandler.java deleted file mode 100644 index b00e47d..0000000 --- a/mvc/src/main/java/project/server/mvc/springframework/handler/RequestHandler.java +++ /dev/null @@ -1,46 +0,0 @@ -package project.server.mvc.springframework.handler;//package project.server.mission.server; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.Socket; -import java.nio.charset.StandardCharsets; -import lombok.extern.slf4j.Slf4j; -import project.server.mvc.servlet.HttpServletRequest; -import project.server.mvc.servlet.HttpServletResponse; -import project.server.mvc.servlet.Request; -import project.server.mvc.servlet.Response; -import project.server.mvc.servlet.Servlet; -import static project.server.mvc.springframework.context.ApplicationContextProvider.getBean; - -@Slf4j -public final class RequestHandler extends Thread { - - private final Socket connection; - private final Servlet dispatcherServlet; - - public RequestHandler(Socket connectionSocket) { - this.connection = connectionSocket; - this.dispatcherServlet = getBean("dispatcherServlet"); - } - - @Override - public void run() { - log.info("Connected IP : {}, Port : {}", connection.getInetAddress(), connection.getPort()); - try ( - InputStream in = connection.getInputStream(); - OutputStream out = connection.getOutputStream(); - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)) - ) { - HttpServletRequest request = new Request(bufferedReader); - HttpServletResponse response = new Response(out); - dispatcherServlet.service(request, response); - } catch (IOException exception) { - log.error(exception.getMessage()); - } catch (Exception exception) { - throw new RuntimeException(exception); - } - } -} diff --git a/mvc/src/main/java/project/server/mvc/springframework/ui/ModelMap.java b/mvc/src/main/java/project/server/mvc/springframework/ui/ModelMap.java index 1333466..7e67279 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/ui/ModelMap.java +++ b/mvc/src/main/java/project/server/mvc/springframework/ui/ModelMap.java @@ -1,11 +1,12 @@ package project.server.mvc.springframework.ui; +import java.util.InvalidPropertiesFormatException; import java.util.LinkedHashMap; import java.util.Map; public class ModelMap { - private final Map map = new LinkedHashMap(); + private final Map map = new LinkedHashMap<>(); public void put( String attributeName, @@ -17,4 +18,11 @@ public void put( public Object getAttribute(String key) { return map.get(key); } + + public String[] keys() { + if (!map.isEmpty()) { + return map.keySet().toArray(new String[0]); + } + return new String[0]; + } } 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 e5d6498..b935917 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 @@ -11,7 +11,8 @@ public abstract class FrameworkServlet extends HttpServletBean { private static final HandlerMethod staticResourceHandlerMethod = new HandlerMethod(new ResourceHttpRequestHandler()); - private static final String STATIC_RESOURCE = "."; + 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 @@ -39,19 +40,20 @@ public void doGet( private boolean isStaticResource(HttpServletRequest request) { String uri = request.getRequestUri(); String[] parsedUri = uri.split("/"); - boolean uriContainsExclude = false; for (String eachUri : parsedUri) { - if (".css".equals(eachUri) || ".png".equals(eachUri) || ".favicon".equals(eachUri)) { + if (EMPTY_STRING.equals(eachUri)) { + continue; + } + if (staticResources.contains(eachUri)) { return true; } } for (String eachUri : parsedUri) { if (excludeStaticResources.contains(eachUri)) { - uriContainsExclude = true; - break; + return false; } } - return uri.contains(STATIC_RESOURCE) && !uriContainsExclude; + return true; } private void processStaticRequest( diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/GlobalExceptionHandler.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/GlobalExceptionHandler.java index 060ed4b..936f20f 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/GlobalExceptionHandler.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/GlobalExceptionHandler.java @@ -1,41 +1,33 @@ package project.server.mvc.springframework.web.servlet; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import project.server.mvc.servlet.HttpServletResponse; import project.server.mvc.servlet.http.HttpStatus; -import static project.server.mvc.servlet.http.HttpStatus.UN_AUTHORIZED; +import static project.server.mvc.servlet.http.HttpStatus.INTERNAL_SERVER_ERROR; import project.server.mvc.springframework.annotation.Component; +import project.server.mvc.springframework.exception.ErrorResponse; @Slf4j @Component public class GlobalExceptionHandler { + private static final ObjectMapper objectMapper = new ObjectMapper(); + public void resolveException( HttpServletResponse response, Exception exception - ) { + ) throws JsonProcessingException { Throwable cause = exception.getCause(); - HttpStatus findStatus = getHttpStatus(cause.getMessage()); - log.error("{code:{}, message:{}}", findStatus.getStatusCode(), cause.getMessage()); - response.setStatus(findStatus); - } - - public HttpStatus getHttpStatus(String message) { - if ("중복된 아이디 입니다.".equals(message)) { - return HttpStatus.BAD_REQUEST; - } - if ("올바른 값을 입력해주세요.".equals(message)) { - return HttpStatus.BAD_REQUEST; + if (cause == null) { + response.setStatus(INTERNAL_SERVER_ERROR); + return; } - if ("이미 가입된 사용자 입니다.".equals(message)) { - return HttpStatus.BAD_REQUEST; - } - if ("사용자를 찾을 수 없습니다.".equals(message)) { - return HttpStatus.NOT_FOUND; - } - if ("권한이 존재하지 않습니다.".equals(message)) { - return UN_AUTHORIZED; - } - return HttpStatus.INTERNAL_SERVER_ERROR; + String stringJson = cause.toString(); + ErrorResponse errorResponse = objectMapper.readValue(stringJson, ErrorResponse.class); + + HttpStatus findStatus = HttpStatus.findByCode(errorResponse.getCode()); + response.setStatus(findStatus); } } 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 c9ba5ae..3dfa7a9 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 @@ -3,22 +3,21 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; +import static java.lang.String.valueOf; import static java.nio.charset.StandardCharsets.UTF_8; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; +import project.server.mvc.servlet.http.HttpStatus; +import static project.server.mvc.servlet.http.HttpStatus.UN_AUTHORIZED; import project.server.mvc.springframework.ui.ModelMap; public class MyInfoView implements View { - private static final String CARRIAGE_RETURN = "\r\n"; private static final int BASE_OFFSET = 0; private static final int DEFAULT_BUFFER_SIZE = 1_024; private static final int EMPTY = -1; - private static final String CONTENT_LENGTH = "Content-Length: "; - private static final String CONTENT_TYPE = "Content-Type: "; - private static final String DELIMITER = " "; + private static final String CONTENT_TYPE = "Content-Type"; + private static final String CONTENT_LENGTH = "Content-Length"; private static final String MASKING = "*"; private static final String STATIC_PREFIX = "static"; @@ -28,72 +27,64 @@ public void render( HttpServletRequest request, HttpServletResponse response ) throws Exception { + HttpStatus httpStatus = response.getStatus(); + if (UN_AUTHORIZED.equals(httpStatus)) { + response.setStatus(UN_AUTHORIZED); + return; + } + ModelMap modelMap = modelAndView.getModelMap(); InputStream inputStream = getInputStream(STATIC_PREFIX + request.getRequestUri()); - byte[] buffer = readStream(inputStream, modelMap); + byte[] buffer = getBuffer(inputStream, modelMap); + setResponseHeader(request, response, buffer.length); - responseBody(response, buffer); + setResponseBody(response, buffer); } private InputStream getInputStream(String path) { - return getClass().getClassLoader().getResourceAsStream(path); - } - - private void responseBody( - HttpServletResponse response, - byte[] body - ) throws IOException { - SocketChannel channel = response.getSocketChannel(); - ByteBuffer buffer = ByteBuffer.wrap(body); - while (buffer.hasRemaining()) { - channel.write(buffer); - } + return getClass().getClassLoader() + .getResourceAsStream(path); } private void setResponseHeader( HttpServletRequest request, HttpServletResponse response, int lengthOfBodyContent - ) throws IOException { - SocketChannel channel = response.getSocketChannel(); - String header = request.getHttpVersion() + DELIMITER + getStatus(response) + CARRIAGE_RETURN - + CONTENT_TYPE - + request.getContentType() - + CARRIAGE_RETURN - + CONTENT_LENGTH - + lengthOfBodyContent - + CARRIAGE_RETURN - + CARRIAGE_RETURN; - ByteBuffer headerBuffer = ByteBuffer.wrap(header.getBytes(UTF_8)); - while (headerBuffer.hasRemaining()) { - channel.write(headerBuffer); - } + ) { + response.setHeader(CONTENT_TYPE, request.getContentType()); + response.setHeader(CONTENT_LENGTH, valueOf(lengthOfBodyContent)); } - private static String getStatus(HttpServletResponse response) { - return response.getStatusAsString() + DELIMITER; + private void setResponseBody( + HttpServletResponse response, + byte[] buffer + ) { + response.setBody(new String(buffer)); } - private byte[] readStream( + private byte[] getBuffer( + InputStream inputStream, + ModelMap modelMap + ) throws IOException { + String convertedHtml = getConvertedHtml(inputStream, modelMap); + return convertedHtml.getBytes(UTF_8); + } + + private static String getConvertedHtml( InputStream inputStream, ModelMap modelMap ) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + int bytesRead; while ((bytesRead = inputStream.read(buffer)) != EMPTY) { byteArrayOutputStream.write(buffer, BASE_OFFSET, bytesRead); } - String convertedHtml = getConvertedHtml(modelMap, byteArrayOutputStream); - return convertedHtml.getBytes(UTF_8); - } - private static String getConvertedHtml( - ModelMap modelMap, - ByteArrayOutputStream byteArrayOutputStream - ) { - Object username = modelMap.getAttribute("username"); - Object password = modelMap.getAttribute("password"); + String[] keys = modelMap.keys(); + Object username = modelMap.getAttribute(keys[0]); + Object password = modelMap.getAttribute(keys[1]); String htmlPage = byteArrayOutputStream.toString(UTF_8); String replacedUsernameHtml = htmlPage.replace(MASKING, username.toString()); diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/RedirectView.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/RedirectView.java index f948dff..1f42b5e 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/RedirectView.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/RedirectView.java @@ -1,22 +1,29 @@ package project.server.mvc.springframework.web.servlet; +import java.io.BufferedReader; import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; -import java.nio.charset.StandardCharsets; +import java.io.InputStream; +import java.io.InputStreamReader; +import static java.lang.String.valueOf; +import static java.nio.charset.StandardCharsets.UTF_8; import java.util.HashMap; import java.util.Map; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; +import static project.server.mvc.servlet.http.HttpStatus.MOVE_PERMANENTLY; public class RedirectView implements View { - private static final String CARRIAGE_RETURN = "\r\n"; private static final String PROTOCOL = "http://"; - private static final String DELIMITER = " "; - private static final String LOCATION_DELIMITER = "Location: "; + private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + private static final String ALL_ORIGIN = "*"; + private static final String LOCATION_DELIMITER = "Location"; private static final String REDIRECT_LOCATION = "redirect:/index.html"; - private static final String HOME = "/index.html"; + private static final String CONTENT_TYPE = "Content-Type"; + private static final String CONTENT_LENGTH = "Content-Length"; + private static final String INDEX_HTML = "/index.html"; + private static final String SIGN_IN_HTML = "static/sign-in.html"; + private static final String TEXT_HTML = "text/html"; public RedirectView() { Map views = new HashMap<>(); @@ -29,41 +36,51 @@ public void render( HttpServletRequest request, HttpServletResponse response ) throws Exception { - response(request, response); + response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, ALL_ORIGIN); + if (MOVE_PERMANENTLY.equals(response.getStatus())) { + response.setHeader(LOCATION_DELIMITER, getRedirectLocation(request)); + return; + } + InputStream inputStream = getInputStream(); + byte[] buffer = getBuffer(inputStream); + setResponseHeader(response, buffer.length); + setResponseBody(response, buffer); } - private void response( - HttpServletRequest request, - HttpServletResponse response - ) throws IOException { - setResponseHeader(request, response); + private String getRedirectLocation(HttpServletRequest request) { + return String.format("%s%s%s", PROTOCOL, request.getHost(), INDEX_HTML); } - private void setResponseHeader(HttpServletRequest request, HttpServletResponse response) throws IOException { - SocketChannel channel = response.getSocketChannel(); - String header = getStartLine(request, response) - + LOCATION_DELIMITER + getRedirectLocation(request) + CARRIAGE_RETURN - + "Access-Control-Allow-Origin: *" + CARRIAGE_RETURN - + "Set-Cookie: " + response.getCookiesAsString() + CARRIAGE_RETURN - + CARRIAGE_RETURN; - ByteBuffer headerBuffer = ByteBuffer.wrap(header.getBytes(StandardCharsets.UTF_8)); - while (headerBuffer.hasRemaining()) { - channel.write(headerBuffer); - } + private InputStream getInputStream() { + return getClass().getClassLoader() + .getResourceAsStream(SIGN_IN_HTML); } - private String getStartLine( - HttpServletRequest request, - HttpServletResponse response - ) { - return String.format("%s%s%s%s", request.getHttpVersion(), DELIMITER, getStatus(response), CARRIAGE_RETURN); + private byte[] getBuffer(InputStream inputStream) throws IOException { + StringBuilder stringBuilder = new StringBuilder(); + InputStreamReader inputStreamReader = new InputStreamReader(inputStream, UTF_8); + try (BufferedReader reader = new BufferedReader(inputStreamReader)) { + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line).append("\n"); + } + } + return stringBuilder.toString() + .getBytes(UTF_8); } - private static String getStatus(HttpServletResponse response) { - return String.format("%s%s", response.getStatusAsString(), DELIMITER); + private void setResponseHeader( + HttpServletResponse response, + long lengthOfBodyContent + ) { + response.setHeader(CONTENT_TYPE, TEXT_HTML); + response.setHeader(CONTENT_LENGTH, valueOf(lengthOfBodyContent)); } - private String getRedirectLocation(HttpServletRequest request) { - return String.format("%s%s%s", PROTOCOL, request.getHost(), HOME); + private void setResponseBody( + HttpServletResponse response, + byte[] buffer + ) { + response.setBody(new String(buffer)); } } diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/StaticView.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/StaticView.java index 66f0c11..65be0be 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/StaticView.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/StaticView.java @@ -4,23 +4,19 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; import static java.nio.charset.StandardCharsets.UTF_8; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; import project.server.mvc.servlet.http.HttpStatus; +import static project.server.mvc.servlet.http.HttpStatus.MOVE_PERMANENTLY; import static project.server.mvc.servlet.http.HttpStatus.UN_AUTHORIZED; public class StaticView implements View { - private static final String CARRIAGE_RETURN = "\r\n"; - private static final String INVALID_COOKIE = "Set-Cookie=; Max-Age=0; Path=/"; - private static final String DELIMITER = " "; - private static final String CONTENT_TYPE = "Content-Type: "; + private static final String CONTENT_TYPE = "Content-Type"; private static final String TEXT_HTML = "text/html"; - private static final String HTML_REQUEST_LINE = CONTENT_TYPE + TEXT_HTML + CARRIAGE_RETURN + CARRIAGE_RETURN; - private static final String INDEX_HTML = "static/index.html"; + private static final String LOCATION = "Location"; + private static final String ROOT = "/"; private static final String NEXT_LINE = "\n"; @Override @@ -29,62 +25,23 @@ public void render( HttpServletRequest request, HttpServletResponse response ) throws Exception { - response(request, response); - } - - private void response( - HttpServletRequest request, - HttpServletResponse response - ) throws IOException { - setResponseHeader(request, response); - } - - private void setResponseHeader( - HttpServletRequest request, - HttpServletResponse response - ) throws IOException { - SocketChannel channel = response.getSocketChannel(); HttpStatus httpStatus = response.getStatus(); - String header = getHeader(request, response); - - ByteBuffer headerBuffer = ByteBuffer.wrap(header.getBytes(UTF_8)); - channel.write(headerBuffer); - if (UN_AUTHORIZED.equals(httpStatus)) { - InputStream inputStream = getInputStream(); - String htmlContent = readInputStream(inputStream); - ByteBuffer contentTypeBuffer = ByteBuffer.wrap(HTML_REQUEST_LINE.getBytes(UTF_8)); - channel.write(contentTypeBuffer); - - ByteBuffer htmlBuffer = ByteBuffer.wrap(htmlContent.getBytes(UTF_8)); - channel.write(htmlBuffer); + response.setStatus(MOVE_PERMANENTLY); + response.setHeader(LOCATION, ROOT); + return; } - } - - private InputStream getInputStream() { - return getClass().getClassLoader() - .getResourceAsStream(INDEX_HTML); - } - private String getHeader( - HttpServletRequest request, - HttpServletResponse response - ) { - HttpStatus httpStatus = response.getStatus(); + InputStream inputStream = getInputStream(request.getRequestUri()); + String html = readInputStream(inputStream); - StringBuilder headerBuilder = new StringBuilder(); - headerBuilder.append(request.getHttpVersion()) - .append(DELIMITER) - .append(httpStatus.getStatusCode()) - .append(DELIMITER) - .append(httpStatus.getStatus()) - .append(CARRIAGE_RETURN); + setResponseHeader(response); + setResponseBody(response, html); + } - if (UN_AUTHORIZED.equals(httpStatus)) { - headerBuilder.append(INVALID_COOKIE) - .append(CARRIAGE_RETURN); - } - return headerBuilder.toString(); + private InputStream getInputStream(String path) { + return getClass().getClassLoader() + .getResourceAsStream(path); } private String readInputStream(InputStream inputStream) throws IOException { @@ -98,4 +55,15 @@ private String readInputStream(InputStream inputStream) throws IOException { } return stringBuilder.toString(); } + + private void setResponseHeader(HttpServletResponse response) { + response.setHeader(CONTENT_TYPE, TEXT_HTML); + } + + private void setResponseBody( + HttpServletResponse response, + String html + ) { + response.setBody(html); + } } 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 a0519d6..df3698c 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 @@ -24,7 +24,8 @@ private String initLookupPath(HttpServletRequest request) { } private String getRequestPath(HttpServletRequest request) { - return request.getRequestUri(); + return request.getRequestUri() + .replace(".html", ""); } private HandlerMethod lookupHandlerMethod( @@ -52,7 +53,7 @@ public MappingRegistry() { new MappingRegistration(new HandlerMethod(homeController)) ); registry.put( - new RequestMappingInfo(HttpMethod.GET, "/my-info.html"), + new RequestMappingInfo(HttpMethod.GET, "/my-info"), new MappingRegistration(new HandlerMethod(userInfoController)) ); registry.put( 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 fd026af..53011a5 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 @@ -2,12 +2,14 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import lombok.extern.slf4j.Slf4j; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; import project.server.mvc.springframework.web.method.HandlerMethod; import project.server.mvc.springframework.web.servlet.HandlerAdapter; import project.server.mvc.springframework.web.servlet.ModelAndView; +@Slf4j public abstract class AbstractHandlerMethodAdapter implements HandlerAdapter { @Override @@ -34,7 +36,9 @@ private ModelAndView invokeHandlerMethod( Object instance = handlerMethod.getHandler(); Object[] args = new Object[]{request, response}; + log.info("args: {}", args); Object result = method.invoke(instance, args); + log.info("result: {}", result); return (ModelAndView) result; } } 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 9ce2f32..900bcf5 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 @@ -5,7 +5,6 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; -import static java.nio.charset.StandardCharsets.UTF_8; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; import static project.server.mvc.servlet.http.HttpStatus.NOT_FOUND; @@ -13,13 +12,12 @@ public class ResourceHttpRequestHandler implements HttpRequestHandler { - private static final String CARRIAGE_RETURN = "\r\n"; private static final int BASE_OFFSET = 0; private static final int START_INDEX = 1; private static final int EMPTY = -1; - private static final String CONTENT_LENGTH = "Content-Length: "; - private static final String CONTENT_TYPE = "Content-Type: "; - private static final String DELIMITER = " "; + private static final int BUFFER_CAPACITY = 1_024; + private static final String CONTENT_LENGTH = "Content-Length"; + private static final String CONTENT_TYPE = "Content-Type"; private static final String HTML = ".html"; private static final String STATIC_PREFIX = "static"; @@ -60,7 +58,8 @@ private String getFile(String uri) { } private InputStream getInputStream(String path) { - return getClass().getClassLoader().getResourceAsStream(path); + return getClass().getClassLoader() + .getResourceAsStream(path); } private void response( @@ -68,24 +67,20 @@ private void response( HttpServletResponse response, InputStream inputStream ) throws IOException { - byte[] buffer = readStream(inputStream); + byte[] buffer = readInputStream(inputStream); setResponseHeader(request, response, buffer.length); - responseBody(response, buffer); + setResponseBody(response, buffer); } private void responsePageNotFound(HttpServletResponse response) { response.setStatus(NOT_FOUND); } - private void responseBody( + private void setResponseBody( HttpServletResponse response, byte[] body ) throws IOException { - SocketChannel channel = response.getSocketChannel(); - ByteBuffer buffer = ByteBuffer.wrap(body); - while (buffer.hasRemaining()) { - channel.write(buffer); - } + response.write(body); } private void setResponseHeader( @@ -93,24 +88,17 @@ private void setResponseHeader( HttpServletResponse response, int lengthOfBodyContent ) throws IOException { - SocketChannel channel = response.getSocketChannel(); - String header = request.getHttpVersion() + DELIMITER + getStatus(response) + CARRIAGE_RETURN + - CONTENT_TYPE + request.getContentType() + CARRIAGE_RETURN + - CONTENT_LENGTH + lengthOfBodyContent + CARRIAGE_RETURN + - CARRIAGE_RETURN; - ByteBuffer headerBuffer = ByteBuffer.wrap(header.getBytes(UTF_8)); - while (headerBuffer.hasRemaining()) { - channel.write(headerBuffer); - } - } + response.getHttpHeaderLine(); + response.setHeader(CONTENT_TYPE, request.getContentType()); + response.setHeader(CONTENT_LENGTH, String.valueOf(lengthOfBodyContent)); - private static String getStatus(HttpServletResponse response) { - return response.getStatusAsString() + DELIMITER; + String header = response.getHttpHeaderLine(); + response.write(header); } - private byte[] readStream(InputStream inputStream) throws IOException { + private byte[] readInputStream(InputStream inputStream) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; + byte[] buffer = new byte[BUFFER_CAPACITY]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != EMPTY) { byteArrayOutputStream.write(buffer, BASE_OFFSET, bytesRead); diff --git a/mvc/src/main/java/project/server/mvc/tomcat/AbstractEndpoint.java b/mvc/src/main/java/project/server/mvc/tomcat/AbstractEndpoint.java index 5fe0057..cb6f207 100644 --- a/mvc/src/main/java/project/server/mvc/tomcat/AbstractEndpoint.java +++ b/mvc/src/main/java/project/server/mvc/tomcat/AbstractEndpoint.java @@ -4,11 +4,13 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public abstract class AbstractEndpoint { +public abstract class AbstractEndpoint { private final ExecutorService executorService; public AbstractEndpoint(ExecutorService executorService) { this.executorService = executorService; } + + protected abstract boolean setSocketOptions(U socket); } diff --git a/mvc/src/main/java/project/server/mvc/tomcat/AsyncRequest.java b/mvc/src/main/java/project/server/mvc/tomcat/AsyncRequest.java deleted file mode 100644 index 50a2fa3..0000000 --- a/mvc/src/main/java/project/server/mvc/tomcat/AsyncRequest.java +++ /dev/null @@ -1,92 +0,0 @@ -package project.server.mvc.tomcat; - -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; -import java.util.ArrayList; -import java.util.List; -import lombok.extern.slf4j.Slf4j; -import project.server.mvc.servlet.HttpServletRequest; -import project.server.mvc.servlet.HttpServletResponse; -import project.server.mvc.servlet.Request; -import project.server.mvc.servlet.Response; -import project.server.mvc.servlet.http.HttpHeaders; -import project.server.mvc.servlet.http.RequestBody; -import project.server.mvc.servlet.http.RequestLine; -import static project.server.mvc.springframework.context.ApplicationContextProvider.getBean; -import project.server.mvc.springframework.web.servlet.DispatcherServlet; - -@Slf4j -public class AsyncRequest implements Runnable { - - private static final int START_OFFSET = 0; - private static final int START_LINE = 0; - private static final String CARRIAGE_RETURN = "\r\n"; - - private final SocketChannel socketChannel; - private final ByteBuffer buffer; - private final DispatcherServlet dispatcherServlet; - - public AsyncRequest( - SocketChannel socketChannel, - ByteBuffer buffer - ) { - this.socketChannel = socketChannel; - this.buffer = buffer; - this.dispatcherServlet = getBean("dispatcherServlet"); - } - - @Override - public void run() { - try { - String httpMessage = new String(buffer.array(), START_OFFSET, buffer.limit()); - String[] lines = httpMessage.split(CARRIAGE_RETURN); - - int index = 1; - List headerLines = new ArrayList<>(); - while (index < lines.length && !lines[index].isEmpty()) { - headerLines.add(lines[index]); - index++; - } - - String requestBody = getRequestBodyBuilder(lines, index); - - try (SocketChannel channel = this.socketChannel; - OutputStream outputStream = channel.socket().getOutputStream()) { - - HttpServletRequest request = createHttpServletRequest(lines, headerLines, requestBody); - HttpServletResponse response = new Response(channel, outputStream); - dispatcherServlet.service(request, response); - } - - } catch (Exception exception) { - log.error("message: {}", exception.getMessage()); - } - } - - private String getRequestBodyBuilder( - String[] lines, - int index - ) { - StringBuilder requestBodyBuilder = new StringBuilder(); - if (index < lines.length) { - for (int subIndex = index + 1; subIndex < lines.length; subIndex++) { - requestBodyBuilder.append(lines[subIndex]) - .append(CARRIAGE_RETURN); - } - } - return requestBodyBuilder.toString(); - } - - private HttpServletRequest createHttpServletRequest( - String[] lines, - List headerLines, - String requestBody - ) { - return new Request( - new RequestLine(lines[START_LINE]), - new HttpHeaders(headerLines), - new RequestBody(requestBody) - ); - } -} diff --git a/mvc/src/main/java/project/server/mvc/tomcat/Nio2EndPoint.java b/mvc/src/main/java/project/server/mvc/tomcat/Nio2EndPoint.java index 404f532..1638682 100644 --- a/mvc/src/main/java/project/server/mvc/tomcat/Nio2EndPoint.java +++ b/mvc/src/main/java/project/server/mvc/tomcat/Nio2EndPoint.java @@ -1,5 +1,6 @@ package project.server.mvc.tomcat; +import java.nio.channels.SocketChannel; import java.util.Objects; import java.util.Queue; import java.util.UUID; @@ -7,7 +8,7 @@ import java.util.concurrent.ExecutorService; import java.util.function.Predicate; -public class Nio2EndPoint extends AbstractEndpoint { +public class Nio2EndPoint extends AbstractJsseEndpoint { private final Poller poller; @@ -16,13 +17,21 @@ public Nio2EndPoint(ExecutorService executorService) { this.poller = new Poller(); } + @Override + public boolean setSocketOptions(SocketChannel socket) { +// PollerEvent pollerEvent = new PollerEvent(socket); + return false; + } + class Poller implements Runnable { private static final Queue events = new ConcurrentLinkedQueue<>(); @Override public void run() { - + while (true) { + break; + } } public void register(NioSocketWrapper socketWrapper) { 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 1f4b284..93c4f7d 100644 --- a/mvc/src/main/java/project/server/mvc/tomcat/NioSocketWrapper.java +++ b/mvc/src/main/java/project/server/mvc/tomcat/NioSocketWrapper.java @@ -1,4 +1,107 @@ package project.server.mvc.tomcat; -public class NioSocketWrapper { +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; +import java.util.ArrayList; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import project.server.mvc.servlet.HttpServletRequest; +import project.server.mvc.servlet.HttpServletResponse; +import project.server.mvc.servlet.Request; +import project.server.mvc.servlet.Response; +import project.server.mvc.servlet.http.HttpHeaders; +import project.server.mvc.servlet.http.RequestBody; +import project.server.mvc.servlet.http.RequestLine; +import static project.server.mvc.springframework.context.ApplicationContextProvider.getBean; +import project.server.mvc.springframework.web.servlet.DispatcherServlet; + +@Slf4j +public class NioSocketWrapper implements Runnable { + + private static final int START_OFFSET = 0; + private static final int START_LINE = 0; + private static final String CARRIAGE_RETURN = "\r\n"; + + private final SocketChannel socketChannel; + private final ByteBuffer buffer; + private final DispatcherServlet dispatcherServlet = getBean("dispatcherServlet"); + private HttpServletResponse response; + + public NioSocketWrapper( + SocketChannel socketChannel, + ByteBuffer buffer + ) throws IOException { + this.socketChannel = socketChannel; + this.buffer = buffer; + socketChannel.read(buffer); + } + + @Override + public void run() { + try { + String httpMessage = new String(buffer.array(), START_OFFSET, buffer.limit()); + String[] lines = httpMessage.split(CARRIAGE_RETURN); + + int index = 1; + List headerLines = new ArrayList<>(); + while (index < lines.length && !lines[index].isEmpty()) { + headerLines.add(lines[index]); + index++; + } + + String requestBody = getRequestBodyBuilder(lines, index); + + try (SocketChannel channel = this.socketChannel) { + HttpServletRequest request = createHttpServletRequest(lines, headerLines, requestBody); + HttpServletResponse response = new Response(channel); + + this.response = response; + dispatcherServlet.service(request, response); + + ByteBuffer headerBuffer = ByteBuffer.wrap(response.toString().getBytes(UTF_8)); + while (headerBuffer.hasRemaining()) { + channel.write(headerBuffer); + } + } + } catch (Exception exception) { + log.error("message: {}", exception.getMessage()); + } + } + + private String getRequestBodyBuilder( + String[] lines, + int index + ) { + StringBuilder requestBodyBuilder = new StringBuilder(); + if (index < lines.length) { + for (int subIndex = index + 1; subIndex < lines.length; subIndex++) { + requestBodyBuilder.append(lines[subIndex]) + .append(CARRIAGE_RETURN); + } + } + return requestBodyBuilder.toString(); + } + + private HttpServletRequest createHttpServletRequest( + String[] lines, + List headerLines, + String requestBody + ) { + return new Request( + new RequestLine(lines[START_LINE]), + new HttpHeaders(headerLines), + new RequestBody(requestBody) + ); + } + + public HttpServletResponse getResponse() { + return response; + } + + public void flip() { + buffer.flip(); + } } diff --git a/settings.gradle b/settings.gradle index 5816a77..2adb816 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name = "spring-server" -include("app", "mvc") +include("app", "mvc", "jdbc")