diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..e3b276e9bf 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -3,7 +3,7 @@ handlebars: server: tomcat: - accept-count: 1 - max-connections: 1 + accept-count: 0 + max-connections: 2 threads: max: 2 diff --git a/study/src/test/java/thread/stage0/SynchronizationTest.java b/study/src/test/java/thread/stage0/SynchronizationTest.java index 0333c18e3b..b463c2b984 100644 --- a/study/src/test/java/thread/stage0/SynchronizationTest.java +++ b/study/src/test/java/thread/stage0/SynchronizationTest.java @@ -41,7 +41,7 @@ private static final class SynchronizedMethods { private int sum = 0; - public void calculate() { + public synchronized void calculate() { setSum(getSum() + 1); } diff --git a/study/src/test/java/thread/stage0/ThreadPoolsTest.java b/study/src/test/java/thread/stage0/ThreadPoolsTest.java index 238611ebfe..03efdabc8d 100644 --- a/study/src/test/java/thread/stage0/ThreadPoolsTest.java +++ b/study/src/test/java/thread/stage0/ThreadPoolsTest.java @@ -31,8 +31,8 @@ void testNewFixedThreadPool() { executor.submit(logWithSleep("hello fixed thread pools")); // 올바른 값으로 바꿔서 테스트를 통과시키자. - final int expectedPoolSize = 0; - final int expectedQueueSize = 0; + final int expectedPoolSize = 2; + final int expectedQueueSize = 1; assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size()); @@ -46,7 +46,7 @@ void testNewCachedThreadPool() { executor.submit(logWithSleep("hello cached thread pools")); // 올바른 값으로 바꿔서 테스트를 통과시키자. - final int expectedPoolSize = 0; + final int expectedPoolSize = 3; final int expectedQueueSize = 0; assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); diff --git a/study/src/test/java/thread/stage1/ConcurrencyTest.java b/study/src/test/java/thread/stage1/ConcurrencyTest.java index f5e8ee070a..947e1ae5fb 100644 --- a/study/src/test/java/thread/stage1/ConcurrencyTest.java +++ b/study/src/test/java/thread/stage1/ConcurrencyTest.java @@ -1,5 +1,6 @@ package thread.stage1; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; diff --git a/tomcat/src/main/java/nextstep/Application.java b/tomcat/src/main/java/nextstep/Application.java index cb8f68b7a8..c722b301a0 100644 --- a/tomcat/src/main/java/nextstep/Application.java +++ b/tomcat/src/main/java/nextstep/Application.java @@ -1,12 +1,52 @@ package nextstep; +import java.util.List; +import java.util.Map; +import nextstep.jwp.controller.Controller; +import nextstep.jwp.controller.HomeController; +import nextstep.jwp.controller.LoginController; +import nextstep.jwp.controller.RegisterController; import nextstep.servlet.DispatcherServletManager; +import nextstep.servlet.StaticResourceResolver; +import nextstep.servlet.interceptor.Interceptor; +import nextstep.servlet.interceptor.SessionInterceptor; import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.http11.message.HttpMethod; +import org.apache.coyote.http11.message.request.RequestLine; public class Application { public static void main(String[] args) { - final var tomcat = new Tomcat(new DispatcherServletManager()); + final var tomcat = configTomcat(); tomcat.start(); } + + private static Tomcat configTomcat() { + final DispatcherServletManager servletManger = configServletManager(); + return new Tomcat(servletManger); + } + + private static DispatcherServletManager configServletManager() { + final Map, Interceptor> interceptors = Map.of( + List.of( + new RequestLine(HttpMethod.GET, "/login"), + new RequestLine(HttpMethod.POST, "/login") + ) + , new SessionInterceptor() + ); + + final List controllers = List.of( + new HomeController(), + new LoginController(), + new RegisterController() + ); + + final StaticResourceResolver staticResourceResolver = new StaticResourceResolver(); + + return new DispatcherServletManager( + interceptors, + controllers, + staticResourceResolver + ); + } } diff --git a/tomcat/src/main/java/nextstep/jwp/controller/AbstractController.java b/tomcat/src/main/java/nextstep/jwp/controller/AbstractController.java new file mode 100644 index 0000000000..64f08d8c7b --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/AbstractController.java @@ -0,0 +1,19 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.http11.message.HttpMethod; +import org.apache.coyote.http11.message.request.HttpRequest; + +public abstract class AbstractController implements Controller { + + @Override + public ResponseEntity handle(HttpRequest request) { + if (request.getMethod() == HttpMethod.GET) { + return doGet(); + } + return doPost(request); + } + + abstract ResponseEntity doGet(); + + abstract ResponseEntity doPost(HttpRequest request); +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/Controller.java b/tomcat/src/main/java/nextstep/jwp/controller/Controller.java index 0771095e33..1ecfdf3c5c 100644 --- a/tomcat/src/main/java/nextstep/jwp/controller/Controller.java +++ b/tomcat/src/main/java/nextstep/jwp/controller/Controller.java @@ -1,11 +1,10 @@ package nextstep.jwp.controller; -import java.io.IOException; -import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.message.request.HttpRequest; public interface Controller { boolean canHandle(HttpRequest request); - ResponseEntity handle(HttpRequest request) throws IOException; + ResponseEntity handle(HttpRequest request); } diff --git a/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java b/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java new file mode 100644 index 0000000000..0bfd026101 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java @@ -0,0 +1,24 @@ +package nextstep.jwp.controller; + +import static nextstep.servlet.StaticResourceResolver.HOME_PAGE; + +import org.apache.coyote.http11.message.HttpStatusCode; +import org.apache.coyote.http11.message.request.HttpRequest; + +public class HomeController extends AbstractController { + + @Override + public boolean canHandle(HttpRequest request) { + return request.isPathMatch("/") || request.isPathMatch("/index.html"); + } + + @Override + ResponseEntity doGet() { + return ResponseEntity.forward(HttpStatusCode.OK, HOME_PAGE); + } + + @Override + ResponseEntity doPost(HttpRequest request) { + throw new IllegalArgumentException("지원하지 않는 HTTP Method 입니다."); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/rest/LoginController.java b/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java similarity index 61% rename from tomcat/src/main/java/nextstep/jwp/controller/rest/LoginController.java rename to tomcat/src/main/java/nextstep/jwp/controller/LoginController.java index 573966f536..e90e66cef9 100644 --- a/tomcat/src/main/java/nextstep/jwp/controller/rest/LoginController.java +++ b/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java @@ -1,26 +1,30 @@ -package nextstep.jwp.controller.rest; +package nextstep.jwp.controller; -import static nextstep.jwp.controller.StaticResourceController.HOME_PAGE; -import static nextstep.jwp.controller.StaticResourceController.UNAUTHORIZED_PAGE; +import static nextstep.servlet.StaticResourceResolver.HOME_PAGE; +import static nextstep.servlet.StaticResourceResolver.LOGIN_PAGE; +import static nextstep.servlet.StaticResourceResolver.UNAUTHORIZED_PAGE; import java.util.NoSuchElementException; -import nextstep.jwp.controller.ResponseEntity; import nextstep.jwp.db.InMemoryUserRepository; import nextstep.jwp.model.User; -import org.apache.coyote.http11.HttpHeaders; -import org.apache.coyote.http11.HttpMethod; -import org.apache.coyote.http11.HttpRequest; -import org.apache.coyote.http11.HttpStatusCode; +import org.apache.coyote.http11.message.HttpHeaders; +import org.apache.coyote.http11.message.HttpStatusCode; +import org.apache.coyote.http11.message.request.HttpRequest; -public class LoginController implements RestController { +public class LoginController extends AbstractController { @Override public boolean canHandle(HttpRequest request) { - return request.getPath().equals("/login") && request.getMethod() == HttpMethod.POST; + return request.isPathMatch("/login"); } @Override - public ResponseEntity handle(HttpRequest request) { + ResponseEntity doGet() { + return ResponseEntity.forward(HttpStatusCode.OK, LOGIN_PAGE); + } + + @Override + ResponseEntity doPost(HttpRequest request) { try { final User user = InMemoryUserRepository.findByAccount(request.getJsonProperty("account")) .orElseThrow(() -> new NoSuchElementException("존재하지 않는 계정입니다.")); @@ -30,7 +34,7 @@ public ResponseEntity handle(HttpRequest request) { session.setAttribute("user", user); headers.setCookie("JSESSIONID", session.getId()); headers.put(HttpHeaders.LOCATION, HOME_PAGE); - return new ResponseEntity(HttpStatusCode.FOUND, headers, ""); + return new ResponseEntity(HttpStatusCode.FOUND, headers, "", true); } return ResponseEntity.found(UNAUTHORIZED_PAGE); } catch (NoSuchElementException e) { diff --git a/tomcat/src/main/java/nextstep/jwp/controller/rest/RegisterController.java b/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java similarity index 64% rename from tomcat/src/main/java/nextstep/jwp/controller/rest/RegisterController.java rename to tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java index e9243c8327..b2763cd4c6 100644 --- a/tomcat/src/main/java/nextstep/jwp/controller/rest/RegisterController.java +++ b/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java @@ -1,23 +1,27 @@ -package nextstep.jwp.controller.rest; +package nextstep.jwp.controller; -import static nextstep.jwp.controller.StaticResourceController.HOME_PAGE; -import static nextstep.jwp.controller.StaticResourceController.REGISTER_PAGE; +import static nextstep.servlet.StaticResourceResolver.HOME_PAGE; +import static nextstep.servlet.StaticResourceResolver.REGISTER_PAGE; -import nextstep.jwp.controller.ResponseEntity; import nextstep.jwp.db.InMemoryUserRepository; import nextstep.jwp.model.User; -import org.apache.coyote.http11.HttpMethod; -import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.message.HttpStatusCode; +import org.apache.coyote.http11.message.request.HttpRequest; -public class RegisterController implements RestController { +public class RegisterController extends AbstractController { @Override public boolean canHandle(HttpRequest request) { - return request.getPath().equals("/register") && request.getMethod() == HttpMethod.POST; + return request.isPathMatch("/register"); } @Override - public ResponseEntity handle(HttpRequest request) { + ResponseEntity doGet() { + return ResponseEntity.forward(HttpStatusCode.OK, REGISTER_PAGE); + } + + @Override + ResponseEntity doPost(HttpRequest request) { final var user = new User( request.getJsonProperty("account"), request.getJsonProperty("password"), diff --git a/tomcat/src/main/java/nextstep/jwp/controller/ResponseEntity.java b/tomcat/src/main/java/nextstep/jwp/controller/ResponseEntity.java index a2ffdc31bf..4f9ed8286f 100644 --- a/tomcat/src/main/java/nextstep/jwp/controller/ResponseEntity.java +++ b/tomcat/src/main/java/nextstep/jwp/controller/ResponseEntity.java @@ -1,29 +1,32 @@ package nextstep.jwp.controller; import java.util.Map; -import org.apache.coyote.http11.HttpHeaders; -import org.apache.coyote.http11.HttpStatusCode; +import org.apache.coyote.http11.message.HttpHeaders; +import org.apache.coyote.http11.message.HttpStatusCode; public class ResponseEntity { private final HttpStatusCode statusCode; private final HttpHeaders headers; private final String body; + private final boolean isRestResponse; - public ResponseEntity(HttpStatusCode statusCode, HttpHeaders headers, String body) { + public ResponseEntity(HttpStatusCode statusCode, HttpHeaders headers, String body, boolean isRestResponse) { this.statusCode = statusCode; this.headers = headers; this.body = body; + this.isRestResponse = isRestResponse; } - public static ResponseEntity ok(String body) { - return new ResponseEntity(HttpStatusCode.OK, HttpHeaders.defaultHeaders(), body); + public static ResponseEntity forward(HttpStatusCode statusCode, String path) { + final var headers = HttpHeaders.defaultHeaders(); + return new ResponseEntity(statusCode, headers, path, false); } public static ResponseEntity found(String location) { final var headers = HttpHeaders.defaultHeaders(); headers.put(HttpHeaders.LOCATION, location); - return new ResponseEntity(HttpStatusCode.FOUND, headers, ""); + return new ResponseEntity(HttpStatusCode.FOUND, headers, "", true); } public String getBody() { @@ -37,4 +40,8 @@ public HttpStatusCode getStatusCode() { public Map getHeaders() { return headers.getHeaders(); } + + public boolean isRestResponse() { + return isRestResponse; + } } diff --git a/tomcat/src/main/java/nextstep/jwp/controller/rest/RestController.java b/tomcat/src/main/java/nextstep/jwp/controller/rest/RestController.java deleted file mode 100644 index 1207b7e2d0..0000000000 --- a/tomcat/src/main/java/nextstep/jwp/controller/rest/RestController.java +++ /dev/null @@ -1,14 +0,0 @@ -package nextstep.jwp.controller.rest; - -import nextstep.jwp.controller.Controller; -import nextstep.jwp.controller.ResponseEntity; -import org.apache.coyote.http11.HttpRequest; - -public interface RestController extends Controller { - - @Override - boolean canHandle(HttpRequest request); - - @Override - ResponseEntity handle(HttpRequest request); -} diff --git a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java index d2f86c307f..a18b82726f 100644 --- a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java +++ b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java @@ -3,26 +3,28 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; import nextstep.jwp.model.User; public class InMemoryUserRepository { private static final Map database = new ConcurrentHashMap<>(); - private static long sequence = 1L; + private static final AtomicLong sequence = new AtomicLong(1); static { - final var user = new User(sequence++, "gugu", "password", "hkkang@woowahan.com"); + final var user = new User(sequence.getAndIncrement(), "gugu", "password", "hkkang@woowahan.com"); database.put(user.getAccount(), user); } + private InMemoryUserRepository() { + } + public static void save(User user) { - final var newUser = new User(sequence++, user.getAccount(), user.getPassword(), user.getEmail()); + final var newUser = new User(sequence.getAndIncrement(), user.getAccount(), user.getPassword(), user.getEmail()); database.put(user.getAccount(), newUser); } public static Optional findByAccount(String account) { return Optional.ofNullable(database.get(account)); } - - private InMemoryUserRepository() {} } diff --git a/tomcat/src/main/java/nextstep/servlet/DispatcherServlet.java b/tomcat/src/main/java/nextstep/servlet/DispatcherServlet.java index b5f894fb43..a73273ab5e 100644 --- a/tomcat/src/main/java/nextstep/servlet/DispatcherServlet.java +++ b/tomcat/src/main/java/nextstep/servlet/DispatcherServlet.java @@ -4,34 +4,26 @@ import java.util.List; import java.util.Map; import nextstep.jwp.controller.Controller; -import nextstep.jwp.controller.StaticResourceController; -import nextstep.jwp.controller.rest.LoginController; -import nextstep.jwp.controller.rest.RegisterController; -import nextstep.servlet.filter.Interceptor; -import nextstep.servlet.filter.SessionInterceptor; +import nextstep.servlet.interceptor.Interceptor; import org.apache.catalina.servlet.Servlet; -import org.apache.coyote.http11.HttpMethod; -import org.apache.coyote.http11.HttpRequest; -import org.apache.coyote.http11.HttpResponse; -import org.apache.coyote.http11.RequestLine; +import org.apache.coyote.http11.message.request.HttpRequest; +import org.apache.coyote.http11.message.request.RequestLine; +import org.apache.coyote.http11.message.response.HttpResponse; public class DispatcherServlet implements Servlet { - private final Map, Interceptor> interceptors = Map.of( - List.of( - new RequestLine(HttpMethod.GET, "/login"), - new RequestLine(HttpMethod.POST, "/login") - ) - , new SessionInterceptor() - ); - - private final List controllers = List.of( - new LoginController(), - new RegisterController(), - new StaticResourceController() - ); - - public DispatcherServlet() { + private final Map, Interceptor> interceptors; + private final List controllers; + private final StaticResourceResolver staticResourceResolver; + + public DispatcherServlet( + final Map, Interceptor> interceptors, + final List controllers, + final StaticResourceResolver staticResourceResolver + ) { + this.interceptors = interceptors; + this.controllers = controllers; + this.staticResourceResolver = staticResourceResolver; } @Override @@ -42,7 +34,13 @@ public void service(HttpRequest request, HttpResponse response) throws IOExcepti response.setStatus(responseEntity.getStatusCode()); responseEntity.getHeaders().forEach(response::setHeader); - response.setBody(responseEntity.getBody()); + + if (responseEntity.isRestResponse()) { + response.setBody(responseEntity.getBody()); + return; + } + final var staticResource = staticResourceResolver.findFileContentByPath(responseEntity.getBody()); + response.setBody(staticResource); } } diff --git a/tomcat/src/main/java/nextstep/servlet/DispatcherServletManager.java b/tomcat/src/main/java/nextstep/servlet/DispatcherServletManager.java index da5ef4a07c..cfea79016d 100644 --- a/tomcat/src/main/java/nextstep/servlet/DispatcherServletManager.java +++ b/tomcat/src/main/java/nextstep/servlet/DispatcherServletManager.java @@ -1,12 +1,35 @@ package nextstep.servlet; +import java.util.List; +import java.util.Map; +import nextstep.jwp.controller.Controller; +import nextstep.servlet.interceptor.Interceptor; import org.apache.catalina.servlet.Servlet; import org.apache.catalina.servlet.ServletManger; +import org.apache.coyote.http11.message.request.RequestLine; public class DispatcherServletManager implements ServletManger { + private final Map, Interceptor> interceptors; + private final List controllers; + private final StaticResourceResolver staticResourceResolver; + + public DispatcherServletManager( + Map, Interceptor> interceptors, + List controllers, + StaticResourceResolver staticResourceResolver + ) { + this.interceptors = interceptors; + this.controllers = controllers; + this.staticResourceResolver = staticResourceResolver; + } + @Override public Servlet createServlet() { - return new DispatcherServlet(); + return new DispatcherServlet( + interceptors, + controllers, + staticResourceResolver + ); } } diff --git a/tomcat/src/main/java/nextstep/jwp/controller/StaticResourceController.java b/tomcat/src/main/java/nextstep/servlet/StaticResourceResolver.java similarity index 51% rename from tomcat/src/main/java/nextstep/jwp/controller/StaticResourceController.java rename to tomcat/src/main/java/nextstep/servlet/StaticResourceResolver.java index 2583f8c71c..3a93184291 100644 --- a/tomcat/src/main/java/nextstep/jwp/controller/StaticResourceController.java +++ b/tomcat/src/main/java/nextstep/servlet/StaticResourceResolver.java @@ -1,52 +1,24 @@ -package nextstep.jwp.controller; +package nextstep.servlet; import java.io.IOException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.util.List; import nextstep.jwp.exception.UncheckedServletException; -import org.apache.coyote.http11.HttpMethod; -import org.apache.coyote.http11.HttpRequest; -import org.apache.coyote.http11.HttpResponse; -public class StaticResourceController implements Controller { +public class StaticResourceResolver { private static final int MAX_DEPTH = 3; private static final String PATH_DELIMITER = "/"; private static final String RESOURCE_ROOT_PATH = "static"; public static final String HOME_PAGE = "/index.html"; + public static final String LOGIN_PAGE = "/login.html"; public static final String UNAUTHORIZED_PAGE = "/401.html"; - public static final String REGISTER_PAGE = "register.html"; + public static final String REGISTER_PAGE = "/register.html"; - private final List staticRequestPaths = List.of( - "/login", - "/register" - ); - - @Override - public boolean canHandle(HttpRequest request) { - final String path = request.getPath(); - return path.substring(path.lastIndexOf("/") + 1).contains(".") || (staticRequestPaths.contains(path) && request.getMethod() == HttpMethod.GET); - } - - @Override - public ResponseEntity handle(HttpRequest request) throws IOException { - return createStaticResponse(request); - } - - private ResponseEntity createStaticResponse(HttpRequest request) throws IOException { - var responseBody = ""; - if (request.getMethod() == HttpMethod.GET) { - responseBody = findFileContentByPath(request.getPath()); - } - - return ResponseEntity.ok(responseBody); - } - - private String findFileContentByPath(String requestPath) throws IOException { - final var resourcePath = HttpResponse.class.getClassLoader().getResource(RESOURCE_ROOT_PATH).getPath(); + public String findFileContentByPath(String requestPath) throws IOException { + final var resourcePath = getClass().getClassLoader().getResource(RESOURCE_ROOT_PATH).getPath(); final var fileName = requestPath.substring(requestPath.lastIndexOf(PATH_DELIMITER) + 1); final var filePath = findAbsolutePath(resourcePath, fileName); diff --git a/tomcat/src/main/java/nextstep/servlet/filter/Interceptor.java b/tomcat/src/main/java/nextstep/servlet/filter/Interceptor.java deleted file mode 100644 index a41ec21a14..0000000000 --- a/tomcat/src/main/java/nextstep/servlet/filter/Interceptor.java +++ /dev/null @@ -1,9 +0,0 @@ -package nextstep.servlet.filter; - -import org.apache.coyote.http11.HttpRequest; -import org.apache.coyote.http11.HttpResponse; - -public interface Interceptor { - - boolean preHandle(HttpRequest request, HttpResponse response); -} diff --git a/tomcat/src/main/java/nextstep/servlet/interceptor/Interceptor.java b/tomcat/src/main/java/nextstep/servlet/interceptor/Interceptor.java new file mode 100644 index 0000000000..7cd5fea928 --- /dev/null +++ b/tomcat/src/main/java/nextstep/servlet/interceptor/Interceptor.java @@ -0,0 +1,9 @@ +package nextstep.servlet.interceptor; + +import org.apache.coyote.http11.message.request.HttpRequest; +import org.apache.coyote.http11.message.response.HttpResponse; + +public interface Interceptor { + + boolean preHandle(HttpRequest request, HttpResponse response); +} diff --git a/tomcat/src/main/java/nextstep/servlet/filter/SessionInterceptor.java b/tomcat/src/main/java/nextstep/servlet/interceptor/SessionInterceptor.java similarity index 65% rename from tomcat/src/main/java/nextstep/servlet/filter/SessionInterceptor.java rename to tomcat/src/main/java/nextstep/servlet/interceptor/SessionInterceptor.java index 8fd7fa79fe..773f6606eb 100644 --- a/tomcat/src/main/java/nextstep/servlet/filter/SessionInterceptor.java +++ b/tomcat/src/main/java/nextstep/servlet/interceptor/SessionInterceptor.java @@ -1,9 +1,9 @@ -package nextstep.servlet.filter; +package nextstep.servlet.interceptor; -import static nextstep.jwp.controller.StaticResourceController.HOME_PAGE; +import static nextstep.servlet.StaticResourceResolver.HOME_PAGE; -import org.apache.coyote.http11.HttpRequest; -import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.message.request.HttpRequest; +import org.apache.coyote.http11.message.response.HttpResponse; public class SessionInterceptor implements Interceptor { diff --git a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java index eef82bbf05..ac097ee0ff 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -4,6 +4,10 @@ import java.io.UncheckedIOException; import java.net.ServerSocket; import java.net.Socket; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.catalina.servlet.ServletManger; import org.apache.coyote.http11.Http11Processor; import org.slf4j.Logger; @@ -15,19 +19,36 @@ public class Connector implements Runnable { private static final int DEFAULT_PORT = 8080; private static final int DEFAULT_ACCEPT_COUNT = 100; + private static final int DEFAULT_THREAD_MAX_NUMBER = 250; private final ServerSocket serverSocket; private final ServletManger servletManger; + private final ExecutorService threadPool; + private final int maxThreadNumber; + private final AtomicInteger workingThreadCount = new AtomicInteger(0); private boolean stopped; + private final Object lock = new Object(); public Connector(ServletManger servletManger) { - this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, servletManger); + this( + DEFAULT_PORT, + DEFAULT_ACCEPT_COUNT, + servletManger, + DEFAULT_THREAD_MAX_NUMBER + ); } - public Connector(final int port, final int acceptCount, ServletManger servletManger) { + public Connector( + final int port, + final int acceptCount, + final ServletManger servletManger, + final int maxThreadNumber + ) { this.servletManger = servletManger; this.serverSocket = createServerSocket(port, acceptCount); this.stopped = false; + this.threadPool = Executors.newFixedThreadPool(maxThreadNumber); + this.maxThreadNumber = maxThreadNumber; } private ServerSocket createServerSocket(final int port, final int acceptCount) { @@ -50,7 +71,6 @@ public void start() { @Override public void run() { - // 클라이언트가 연결될때까지 대기한다. while (!stopped) { connect(); } @@ -58,18 +78,50 @@ public void run() { private void connect() { try { + waitUntilThreadAvailable(); process(serverSocket.accept()); } catch (IOException e) { log.error(e.getMessage(), e); } } + private void waitUntilThreadAvailable() { + if (workingThreadCount.get() >= maxThreadNumber) { + synchronized (lock) { + try { + lock.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error(e.getMessage(), e); + } + } + } + } + private void process(final Socket connection) { if (connection == null) { return; } - var processor = new Http11Processor(connection, servletManger); - new Thread(processor).start(); + workingThreadCount.incrementAndGet(); + final var processor = new Http11Processor(connection, servletManger); + final var task = CompletableFuture.runAsync(processor, threadPool); + decrementWorkingThreadCountIfComplete(task); + } + + private void decrementWorkingThreadCountIfComplete(CompletableFuture task) { + task.whenCompleteAsync((result, throwable) -> { + workingThreadCount.decrementAndGet(); + releaseLockIfThreadAvailable(); + } + ); + } + + private synchronized void releaseLockIfThreadAvailable() { + if (workingThreadCount.get() < maxThreadNumber) { + synchronized (lock) { + lock.notifyAll(); + } + } } public void stop() { diff --git a/tomcat/src/main/java/org/apache/catalina/servlet/Servlet.java b/tomcat/src/main/java/org/apache/catalina/servlet/Servlet.java index 6a4c134dd5..e9f700f231 100644 --- a/tomcat/src/main/java/org/apache/catalina/servlet/Servlet.java +++ b/tomcat/src/main/java/org/apache/catalina/servlet/Servlet.java @@ -1,8 +1,8 @@ package org.apache.catalina.servlet; import java.io.IOException; -import org.apache.coyote.http11.HttpRequest; -import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.message.request.HttpRequest; +import org.apache.coyote.http11.message.response.HttpResponse; public interface Servlet { diff --git a/tomcat/src/main/java/org/apache/catalina/session/Session.java b/tomcat/src/main/java/org/apache/catalina/session/Session.java index b799ce56c4..f1dbb5600a 100644 --- a/tomcat/src/main/java/org/apache/catalina/session/Session.java +++ b/tomcat/src/main/java/org/apache/catalina/session/Session.java @@ -8,9 +8,8 @@ public class Session { private final String id; private final Map values = new HashMap<>(); - public Session(final String id, final SessionManager sessionManager) { + public Session(final String id) { this.id = id; - sessionManager.add(this); } public String getId() { diff --git a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java index 80644e45ed..af34f848a4 100644 --- a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -1,6 +1,7 @@ package org.apache.catalina.session; import java.util.Map; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; public class SessionManager implements Manager { @@ -9,14 +10,20 @@ public class SessionManager implements Manager { private static final class SessionManagerHolder { - private static final SessionManager INSTANCE = new SessionManager(); + private static final SessionManager sessionManager = new SessionManager(); } private SessionManager() { } public static SessionManager getInstance() { - return SessionManagerHolder.INSTANCE; + return SessionManagerHolder.sessionManager; + } + + public Session createSession() { + final var session = new Session(UUID.randomUUID().toString()); + add(session); + return session; } @Override diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index ecff1d15f6..33dfdfd8fb 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -8,6 +8,8 @@ import nextstep.jwp.exception.UncheckedServletException; import org.apache.catalina.servlet.ServletManger; import org.apache.coyote.Processor; +import org.apache.coyote.http11.message.request.HttpRequest; +import org.apache.coyote.http11.message.response.HttpResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/message/HttpHeaders.java similarity index 96% rename from tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java rename to tomcat/src/main/java/org/apache/coyote/http11/message/HttpHeaders.java index 73ab64fef7..834cdd7ef5 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/HttpHeaders.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.message; import java.util.Arrays; import java.util.HashMap; @@ -6,6 +6,7 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import org.apache.coyote.http11.message.request.HttpRequest; public class HttpHeaders { @@ -13,7 +14,6 @@ public class HttpHeaders { public static final String CONTENT_TYPE = "Content-Type"; public static final String CONTENT_TYPE_UTF_8 = "text/html;charset=utf-8"; public static final String CONTENT_TYPE_CSS = "text/css"; - public static final String CONTENT_TYPE_APPLICATION_JSON = "application/json"; public static final String CONTENT_LENGTH = "Content-Length"; public static final String ACCEPT = "Accept"; public static final String LOCATION = "Location"; @@ -45,7 +45,7 @@ public static HttpHeaders createBasicRequestHeadersFrom(List request) { public static HttpHeaders createBasicResponseHeadersFrom(HttpRequest request) { final var headers = new HashMap(); - headers.put(CONTENT_TYPE, getContentTypeFrom(request.getHeaders().get(ACCEPT))); + headers.put(CONTENT_TYPE, getContentTypeFrom(request.getHeaderValue(ACCEPT))); headers.put(CONTENT_LENGTH, DEFAULT_CONTENT_LENGTH); return new HttpHeaders(headers); } @@ -68,7 +68,7 @@ public boolean hasCookie(String key) { return headers.containsKey(COOKIE) && headers.get(COOKIE).contains(key); } - private String get(String key) { + public String get(String key) { return headers.get(key); } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/message/HttpMethod.java similarity index 85% rename from tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java rename to tomcat/src/main/java/org/apache/coyote/http11/message/HttpMethod.java index afd353352a..8cd4704cda 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/HttpMethod.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.message; public enum HttpMethod { GET, diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpStatusCode.java b/tomcat/src/main/java/org/apache/coyote/http11/message/HttpStatusCode.java similarity index 95% rename from tomcat/src/main/java/org/apache/coyote/http11/HttpStatusCode.java rename to tomcat/src/main/java/org/apache/coyote/http11/message/HttpStatusCode.java index d2f1d33eea..25b88c5833 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpStatusCode.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/HttpStatusCode.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.message; public enum HttpStatusCode { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/message/request/HttpRequest.java similarity index 70% rename from tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java rename to tomcat/src/main/java/org/apache/coyote/http11/message/request/HttpRequest.java index bc5584f57c..510ab7e80d 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/request/HttpRequest.java @@ -1,29 +1,25 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.message.request; import java.io.BufferedReader; import java.io.IOException; import java.util.ArrayList; import java.util.Optional; -import java.util.UUID; import org.apache.catalina.session.Session; import org.apache.catalina.session.SessionManager; +import org.apache.coyote.http11.message.HttpHeaders; +import org.apache.coyote.http11.message.HttpMethod; public class HttpRequest { - private static final String HEADER_BODY_DELIMITER = ""; - private static final String INDEX_HTML = "/index.html"; private static final int URI_INDEX = 0; - private final RequestLine requestLine; private final HttpHeaders headers; - private final String body; private final JsonProperties jsonProperties; - private HttpRequest(RequestLine requestLine, HttpHeaders headers,String body, JsonProperties jsonProperties) { + private HttpRequest(RequestLine requestLine, HttpHeaders headers, JsonProperties jsonProperties) { this.requestLine = requestLine; this.headers = headers; - this.body = body; this.jsonProperties = jsonProperties; } @@ -38,7 +34,6 @@ public static HttpRequest from(final BufferedReader reader) throws IOException { } final var header = HttpHeaders.createBasicRequestHeadersFrom(request); - request.add(HEADER_BODY_DELIMITER); if (header.getContentLength() > 0) { var bodyChars = new char[header.getContentLength()]; reader.read(bodyChars, 0, bodyChars.length); @@ -49,24 +44,24 @@ public static HttpRequest from(final BufferedReader reader) throws IOException { final String[] uri = request.get(URI_INDEX).split(" "); var requestLine = RequestLine.from(uri); - return new HttpRequest(requestLine, header, body, properties); - } - - public boolean hasQueryStrings() { - return requestLine.hasQueryStrings(); + return new HttpRequest(requestLine, header, properties); } public boolean hasCookie(String key) { return headers.hasCookie(key); } + public boolean isPathMatch(String path) { + return requestLine.isPathMatch(path); + } + public Session getSession() { final Optional sessionId = headers.getCookie("JSESSIONID"); final var sessionManager = SessionManager.getInstance(); final var localSession = sessionManager.findSession(sessionId.orElse("")); if (localSession == null) { - return new Session(UUID.randomUUID().toString(), sessionManager); + return sessionManager.createSession(); } return localSession; } @@ -75,28 +70,15 @@ public RequestLine getRequestLine() { return requestLine; } - public HttpHeaders getHeaders() { - return headers; + public String getHeaderValue(String key) { + return headers.get(key); } public HttpMethod getMethod() { return requestLine.getMethod(); } - public String getQueryString(String key) { - return requestLine.getQueryStrings().getValue(key); - } - public String getJsonProperty(String key) { return jsonProperties.getValue(key); } - - public String getPath() { - var path = requestLine.getPath(); - if (path.equals("/")) { - return INDEX_HTML; - } - return path; - } - } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/JsonProperties.java b/tomcat/src/main/java/org/apache/coyote/http11/message/request/JsonProperties.java similarity index 86% rename from tomcat/src/main/java/org/apache/coyote/http11/JsonProperties.java rename to tomcat/src/main/java/org/apache/coyote/http11/message/request/JsonProperties.java index 53fed1397e..53272d0f0d 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/JsonProperties.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/request/JsonProperties.java @@ -1,17 +1,18 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.message.request; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Map; import java.util.stream.Collectors; +import org.apache.coyote.http11.message.HttpHeaders; public class JsonProperties { - private final Map jsonProperties; + private final Map properties; - public JsonProperties(Map jsonProperties) { - this.jsonProperties = jsonProperties; + public JsonProperties(Map properties) { + this.properties = properties; } private static JsonProperties fromUrlEncodedForm(String body) { @@ -46,6 +47,6 @@ private static JsonProperties fromJsonForm(String body) { } public String getValue(String key) { - return jsonProperties.get(key); + return properties.get(key); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/QueryStrings.java b/tomcat/src/main/java/org/apache/coyote/http11/message/request/QueryStrings.java similarity index 85% rename from tomcat/src/main/java/org/apache/coyote/http11/QueryStrings.java rename to tomcat/src/main/java/org/apache/coyote/http11/message/request/QueryStrings.java index 61e4134ff3..e49444f744 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/QueryStrings.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/request/QueryStrings.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.message.request; import java.util.Arrays; import java.util.Collections; @@ -21,9 +21,9 @@ public QueryStrings(String fullPath) { if (fullPath.contains("?")) { var queryParams = fullPath.split(PATH_QUERY_STRING_DELIMITER)[1]; this.queryStrings = Arrays.stream(queryParams.split(PATH_QUERY_DELIMITER)) - .map(queryString -> + .map(rawQueryString -> { - final var keyValue = queryString.split(KEY_VALUE_DELIMITER); + final var keyValue = rawQueryString.split(KEY_VALUE_DELIMITER); return Map.entry(keyValue[0], keyValue[1]); } ) @@ -36,8 +36,4 @@ public QueryStrings(String fullPath) { public boolean hasQueryStrings() { return !queryStrings.isEmpty(); } - - public String getValue(String key) { - return queryStrings.get(key); - } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/RequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/message/request/RequestLine.java similarity index 83% rename from tomcat/src/main/java/org/apache/coyote/http11/RequestLine.java rename to tomcat/src/main/java/org/apache/coyote/http11/message/request/RequestLine.java index 1c210ef540..371727b619 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/RequestLine.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/request/RequestLine.java @@ -1,6 +1,7 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.message.request; import java.util.Objects; +import org.apache.coyote.http11.message.HttpMethod; public class RequestLine { @@ -34,26 +35,14 @@ public static RequestLine from(String[] uri) { return new RequestLine(method, path, protocol, queryStrings); } - public boolean hasQueryStrings() { - return queryStrings.hasQueryStrings(); + public boolean isPathMatch(String path) { + return this.path.equals(path); } public HttpMethod getMethod() { return method; } - public String getPath() { - return path; - } - - public String getProtocol() { - return protocol; - } - - public QueryStrings getQueryStrings() { - return queryStrings; - } - @Override public boolean equals(Object o) { if (this == o) { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/message/response/HttpResponse.java similarity index 82% rename from tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java rename to tomcat/src/main/java/org/apache/coyote/http11/message/response/HttpResponse.java index 0121f831d4..ba14737251 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/response/HttpResponse.java @@ -1,8 +1,11 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.message.response; -import static org.apache.coyote.http11.HttpHeaders.HTTP_LINE_SUFFIX; +import static org.apache.coyote.http11.message.HttpHeaders.HTTP_LINE_SUFFIX; import java.nio.charset.StandardCharsets; +import org.apache.coyote.http11.message.HttpHeaders; +import org.apache.coyote.http11.message.HttpStatusCode; +import org.apache.coyote.http11.message.request.HttpRequest; public class HttpResponse { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponseStatusLine.java b/tomcat/src/main/java/org/apache/coyote/http11/message/response/HttpResponseStatusLine.java similarity index 76% rename from tomcat/src/main/java/org/apache/coyote/http11/HttpResponseStatusLine.java rename to tomcat/src/main/java/org/apache/coyote/http11/message/response/HttpResponseStatusLine.java index e8fc39afc0..bdb5e3c83a 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponseStatusLine.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/response/HttpResponseStatusLine.java @@ -1,4 +1,6 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.message.response; + +import org.apache.coyote.http11.message.HttpStatusCode; public class HttpResponseStatusLine { diff --git a/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java index fc4546725d..87e227df36 100644 --- a/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java @@ -9,15 +9,55 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.List; +import java.util.Map; +import nextstep.jwp.controller.Controller; +import nextstep.jwp.controller.HomeController; +import nextstep.jwp.controller.LoginController; +import nextstep.jwp.controller.RegisterController; import nextstep.jwp.db.InMemoryUserRepository; import nextstep.jwp.model.User; import nextstep.servlet.DispatcherServletManager; +import nextstep.servlet.StaticResourceResolver; +import nextstep.servlet.interceptor.Interceptor; +import nextstep.servlet.interceptor.SessionInterceptor; +import org.apache.catalina.servlet.ServletManger; import org.apache.coyote.http11.Http11Processor; +import org.apache.coyote.http11.message.HttpMethod; +import org.apache.coyote.http11.message.request.RequestLine; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import support.StubSocket; class Http11ProcessorTest { + + ServletManger servletManager; + + @BeforeEach + void setUp() { + final Map, Interceptor> interceptors = Map.of( + List.of( + new RequestLine(HttpMethod.GET, "/login"), + new RequestLine(HttpMethod.POST, "/login") + ) + , new SessionInterceptor() + ); + + final List controllers = List.of( + new HomeController(), + new LoginController(), + new RegisterController() + ); + + final StaticResourceResolver staticResourceResolver = new StaticResourceResolver(); + + servletManager = new DispatcherServletManager( + interceptors, + controllers, + staticResourceResolver + ); + } + @Nested class Index { @@ -33,7 +73,7 @@ void fullUrl() throws IOException { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket, new DispatcherServletManager()); + final Http11Processor processor = new Http11Processor(socket, servletManager); // when processor.process(socket); @@ -60,7 +100,7 @@ void defaultUrl() throws IOException { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket, new DispatcherServletManager()); + final Http11Processor processor = new Http11Processor(socket, servletManager); // when processor.process(socket); @@ -91,7 +131,7 @@ void getLogin() throws IOException { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket, new DispatcherServletManager()); + final Http11Processor processor = new Http11Processor(socket, servletManager); // when processor.process(socket); @@ -121,7 +161,7 @@ void postLogin() { InMemoryUserRepository.save(new User(1L, "gugu", "password", "ttset@dsffd.com")); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket, new DispatcherServletManager()); + final Http11Processor processor = new Http11Processor(socket, servletManager); // when processor.process(socket); @@ -145,7 +185,7 @@ void postLoginWithInvalidPassword() { InMemoryUserRepository.save(new User(1L, "gugu", "password", "ttset@dsffd.com")); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket, new DispatcherServletManager()); + final Http11Processor processor = new Http11Processor(socket, servletManager); // when processor.process(socket); @@ -174,7 +214,7 @@ void getRegister() throws IOException { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket, new DispatcherServletManager()); + final Http11Processor processor = new Http11Processor(socket, servletManager); // when processor.process(socket); @@ -205,7 +245,7 @@ void postRegister() { final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket, new DispatcherServletManager()); + final Http11Processor processor = new Http11Processor(socket, servletManager); // when processor.process(socket); diff --git a/tomcat/src/test/java/org/apache/catalina/connector/ConnectorTest.java b/tomcat/src/test/java/org/apache/catalina/connector/ConnectorTest.java new file mode 100644 index 0000000000..d681eb9325 --- /dev/null +++ b/tomcat/src/test/java/org/apache/catalina/connector/ConnectorTest.java @@ -0,0 +1,145 @@ +package org.apache.catalina.connector; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import org.apache.catalina.servlet.Servlet; +import org.apache.catalina.servlet.ServletManger; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ConnectorTest { + + @DisplayName("스레드 크기가 2일 때, 요청이 4개가 들어오면 2개의 스레드만 사용하여 처리한다.") + @Test + void test() throws InterruptedException { + // given + final int port = 63300; + final Connector connector = new Connector( + port, + 250, + new StubServletContainer(), + 2 + ); + connector.start(); + final var latch = new CountDownLatch(4); + + // when + for (int i = 0; i < 4; i++) { + final TestHttpUtils testHttpUtils = new TestHttpUtils(latch, port); + final Thread thread = new Thread(() -> testHttpUtils.send("/index.html")); + thread.start(); + } + latch.await(); + + // then + final var completedTasks = StubServletContainer.map.values() + .stream() + .mapToInt(Integer::intValue) + .sum(); + assertAll( + () -> assertThat(StubServletContainer.map).hasSize(2), + () -> assertThat(completedTasks).isEqualTo(4) + ); + } + + @DisplayName("스레드 크기가 1 이고, acceptCount가 1 이라면 나머지 요청은 수행되지 못한다.") + @Test + void acceptCountTest() throws InterruptedException { + // given + final int port = 63303; + final var connector = new Connector( + port, + 1, + new StubServletContainerWaitForever(), + 1 + ); + connector.start(); + final var latch = new CountDownLatch(10); + final var testHttpUtils2 = new TestHttpUtils(latch, port); + + // when + for (int i = 0; i < 10; i++) { + final var testHttpUtils = new TestHttpUtils(latch, port); + final var thread = new Thread(() -> testHttpUtils.send("/index.html")); + thread.start(); + } + Thread.sleep(300); + final var completedTasks = StubServletContainerWaitForever.map.values() + .stream() + .mapToInt(Integer::valueOf) + .sum(); + + // then + assertAll( + () -> assertThatThrownBy(() -> testHttpUtils2.send("/index.html")).isInstanceOf(RuntimeException.class), + () -> assertThat(completedTasks).isEqualTo(1) + ); + } + + public static class StubServletContainer implements ServletManger { + + public static ConcurrentHashMap map = new ConcurrentHashMap<>(); + + @Override + public Servlet createServlet() { + return (httpRequest, httpResponse) -> map.merge(Thread.currentThread().getName(), 1, Integer::sum); + } + } + + public static class StubServletContainerWaitForever implements ServletManger { + + public static ConcurrentHashMap map = new ConcurrentHashMap<>(); + + @Override + public Servlet createServlet() { + return (httpRequest, httpResponse) -> { + map.merge(Thread.currentThread().getName(), 1, Integer::sum); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }; + } + } + + public static class TestHttpUtils { + + private final HttpClient httpClient; + private final CountDownLatch latch; + private final int port; + + public TestHttpUtils(CountDownLatch latch, int port) { + this.httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .connectTimeout(Duration.ofSeconds(1)) + .build(); + this.latch = latch; + this.port = port; + } + + public void send(final String path) { + try { + final var request = HttpRequest.newBuilder() + .header("Accept", "*/*") + .uri(URI.create("http://localhost:" + port + path)) + .timeout(Duration.ofSeconds(1)) + .build(); + httpClient.send(request, BodyHandlers.ofString()); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + latch.countDown(); + } + } + } +}