Skip to content
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
# 톰캣 구현하기

## 기능 요구 사항

- [x] http://localhost:8080/index.html 페이지에 접근 가능하다.
- [x] 접근한 페이지의 js, css 파일을 불러올 수 있다.
- [x] uri의 QueryString을 파싱하는 기능이 있다.
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package nextstep.jwp.db;

import nextstep.jwp.model.User;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import nextstep.jwp.model.User;

public class InMemoryUserRepository {

Expand All @@ -23,5 +22,6 @@ public static Optional<User> findByAccount(String account) {
return Optional.ofNullable(database.get(account));
}

private InMemoryUserRepository() {}
private InMemoryUserRepository() {
}
}
28 changes: 10 additions & 18 deletions tomcat/src/main/java/org/apache/catalina/Manager.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
package org.apache.catalina;

import jakarta.servlet.http.HttpSession;

import java.io.IOException;

/**
* A <b>Manager</b> manages the pool of Sessions that are associated with a
* particular Container. Different Manager implementations may support
* value-added features such as the persistent storage of session data,
* as well as migrating sessions for distributable web applications.
* A <b>Manager</b> manages the pool of Sessions that are associated with a particular Container. Different Manager
* implementations may support value-added features such as the persistent storage of session data, as well as migrating
* sessions for distributable web applications.
* <p>
* In order for a <code>Manager</code> implementation to successfully operate
* with a <code>Context</code> implementation that implements reloading, it
* must obey the following constraints:
* In order for a <code>Manager</code> implementation to successfully operate with a <code>Context</code> implementation
* that implements reloading, it must obey the following constraints:
* <ul>
* <li>Must implement <code>Lifecycle</code> so that the Context can indicate
* that a restart is required.
Expand All @@ -32,18 +29,13 @@ public interface Manager {
void add(HttpSession session);

/**
* Return the active Session, associated with this Manager, with the
* specified session id (if any); otherwise return <code>null</code>.
* Return the active Session, associated with this Manager, with the specified session id (if any); otherwise return
* <code>null</code>.
*
* @param id The session id for the session to be returned
*
* @exception IllegalStateException if a new session cannot be
* instantiated for any reason
* @exception IOException if an input/output error occurs while
* processing this request
*
* @return the request session or {@code null} if a session with the
* requested ID could not be found
* @return the request session or {@code null} if a session with the requested ID could not be found
* @throws IllegalStateException if a new session cannot be instantiated for any reason
* @throws IOException if an input/output error occurs while processing this request
*/
HttpSession findSession(String id) throws IOException;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package org.apache.catalina.connector;

import org.apache.coyote.http11.Http11Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.ServerSocket;
import java.net.Socket;
import org.apache.coyote.http11.Http11Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Connector implements Runnable {

Expand Down
3 changes: 1 addition & 2 deletions tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package org.apache.catalina.startup;

import java.io.IOException;
import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

public class Tomcat {

private static final Logger log = LoggerFactory.getLogger(Tomcat.class);
Expand Down
5 changes: 2 additions & 3 deletions tomcat/src/main/java/org/apache/coyote/Processor.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@
public interface Processor {

/**
* Process a connection. This is called whenever an event occurs (e.g. more
* data arrives) that allows processing to continue for a connection that is
* not currently being processed.
* Process a connection. This is called whenever an event occurs (e.g. more data arrives) that allows processing to
* continue for a connection that is not currently being processed.
*/
void process(Socket socket);
}
31 changes: 31 additions & 0 deletions tomcat/src/main/java/org/apache/coyote/http11/Extension.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.apache.coyote.http11;

import java.util.Arrays;

public enum Extension {
HTML("html", "text/html"),
CSS("css", "text/css"),
JS("js", "text/js"),
CSV("svg", "image/svg+xml"),
ICO("ico", "image/x-icon"),
;
private final String name;
private final String contentType;

Extension(String name, String contentType) {
this.name = name;
this.contentType = contentType;
}

public static String convertToContentType(String url) {
int index = url.indexOf(".");
if (index == -1) {
return HTML.contentType;
}
Extension extension1 = Arrays.stream(values())
.filter(extension -> url.endsWith(extension.name))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("알맞은 확장자가 없습니다."));
return extension1.contentType;
}
}
Comment on lines +1 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 Extension enum 멋있네요! 근데 클래스 네이밍을 ContentType으로 바꾸는게 좀 더 가독에 좋을 것 같기도??
추가로 convertToContentType()메서드에서 extension1이라는 변수명 바꾸면 더 좋을 것 같습니다👍🏻

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 Extension을 보고 ContentType으로 바꿔주는 클래스였는데 가독성이 오히려 떨어졌군요..! ContentType으로 클래스 네이밍 수정했습니다!

67 changes: 55 additions & 12 deletions tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
package org.apache.coyote.http11;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.util.Objects;
import nextstep.jwp.exception.UncheckedServletException;
import org.apache.coyote.Processor;
import org.apache.coyote.http11.url.HandlerMapping;
import org.apache.coyote.http11.url.Url;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.Socket;

public class Http11Processor implements Runnable, Processor {

private static final Logger log = LoggerFactory.getLogger(Http11Processor.class);
private static final String STATIC_DIRECTORY = "static/";

private final Socket connection;

Expand All @@ -26,21 +35,55 @@ public void run() {
@Override
public void process(final Socket connection) {
try (final var inputStream = connection.getInputStream();
final var outputStream = connection.getOutputStream()) {
final var outputStream = connection.getOutputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
Comment on lines +39 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

놓칠 수 있는 부분인데 Reader도 try with resources로 처리해준 것 좋네요👍🏻


final var responseBody = "Hello world!";
String uri = getUri(bufferedReader);
String contentType = Extension.convertToContentType(uri);
String responseBody = getResponseBody(uri);

final var response = String.join("\r\n",
"HTTP/1.1 200 OK ",
"Content-Type: text/html;charset=utf-8 ",
"Content-Length: " + responseBody.getBytes().length + " ",
"",
responseBody);
final var response = createResponse(contentType, responseBody);

outputStream.write(response.getBytes());
outputStream.flush();
} catch (IOException | UncheckedServletException e) {
} catch (IOException | UncheckedServletException | URISyntaxException e) {
log.error(e.getMessage(), e);
}
}

private String getUri(final BufferedReader bufferedReader) throws IOException {
return bufferedReader.readLine()
.split(" ")[1]
.substring(1);
}

private String getResponseBody(final String uri) throws IOException, URISyntaxException {

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의도하신 개행인가요??

if (uri.isEmpty()) {
return "Hello world!";
}
Url url = HandlerMapping.from(uri);
URL resource = this.getClass()
.getClassLoader()
.getResource(STATIC_DIRECTORY + url.getRequest().getPath());
Comment on lines +66 to +69
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 아주 적절한 추상화인 것 같네요! 한 수 배워갑니다.👍🏻
다만 완전 취향 차이인 것 같은데 저 같으면 url.getRequest().getPath()를 Processor에서 url.getPath()하나로 보여줬을 것 같네요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 해당 부분이 getRequest내부에서 path경로를 수정한 후, path를 반환하는 것이라 하나로 합치기에 어렵더라구요.. 아예 http11Request에서 분리를 해야할 것 같은데 2단계 수정하면서 반영해보겠습니다!


validatePath(resource);
return new String(Files.readAllBytes(new File(resource.getFile()).toPath()));
}

private void validatePath(URL resource) {
if (Objects.isNull(resource)) {
throw new IllegalArgumentException("경로가 잘못 되었습니다. : null");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resources 파일 보니까 HTTP 400, 500대 상태코드에 대한 .html파일이 있더라고요.(저도 적용은 안함ㅎ)
다음 단계 쭉쭉 진행해보면서 적용해 보면 좋을 것 같습니다👍🏻👍🏻

}
}

private String createResponse(String contentType, String responseBody) {
return String.join("\r\n",
"HTTP/1.1 200 OK ",
"Content-Type: " + contentType + ";charset=utf-8 ",
"Content-Length: " + responseBody.getBytes().length + " ",
"",
responseBody);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.apache.coyote.http11.dto;

public class Http11Request {

private final String account;
private final String password;
private final String path;

public Http11Request(String path) {
this(null, null, path);
}

public Http11Request(String account, String password, String path) {
this.account = account;
this.password = password;
this.path = path;
}

public String getAccount() {
return account;
}

public String getPath() {
return path;
}
}
Comment on lines +1 to +26
Copy link

@Yboyu0u Yboyu0u Sep 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위 클래스에서 accout, password 변수는 사실 상 Login할 때 queryString 처리 용으로만 쓰고 있네요.
path는 계속 사용하니까 놔두고 다른 쿼리 처리를 위해 account, password 같은 쿼리 파라미터를 위한 변수들은 Map을 사용한 일급 컬렉션으로 대체해도 좋을 것 같기도??

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵! 위에 남긴 것 처럼 분리가 필요할 것 같네요!

19 changes: 19 additions & 0 deletions tomcat/src/main/java/org/apache/coyote/http11/url/Empty.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.apache.coyote.http11.url;

import org.apache.coyote.http11.dto.Http11Request;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Empty extends Url {
private static final Logger log = LoggerFactory.getLogger(Empty.class);

public Empty(final String url) {
super(url);
}

@Override
public Http11Request getRequest() {
log.info("path : {} ", getPath());
throw new IllegalArgumentException("경로가 비어있습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.apache.coyote.http11.url;

public class HandlerMapping {

public static Url from(String uri) {
if (isHomeUrl(uri)) {
return new HomePage(uri);
}
if (uri.startsWith("login")) {
return new Login(uri);
}
Comment on lines +9 to +11
Copy link

@Yboyu0u Yboyu0u Sep 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 localhost:8080/index.html로 접속 후, 우측 상단의 로그인 버튼을 누르면 localhost:8080/loginuri로 요청이 들어오게 되는데(사실 확장자가 없는 요청이라서 url이 이상한 것ㄱ 같기도?), 저렇게 들어오게 되면 new Login()이 생성되고 StringParser에서 파싱하는 과정에서 아래와 같은 에러가 뜨게 되네요! 쿼리 스트링이 있는 경우에만 동작하는 로직이라 그런 것 같습니다.
image

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗! 그렇네요 체크리스트만 신경쓰다가 해당 부분을 신경 못썼군요.. 수정했습니다!

return new Empty(uri);
}

private static boolean isHomeUrl(String uri) {
return uri.startsWith("index") || uri.endsWith("css") || uri.endsWith("csv") || uri.endsWith("js")
|| uri.endsWith("ico");
}
}
15 changes: 15 additions & 0 deletions tomcat/src/main/java/org/apache/coyote/http11/url/HomePage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.apache.coyote.http11.url;

import org.apache.coyote.http11.dto.Http11Request;

public class HomePage extends Url {

public HomePage(final String url) {
super(url);
}

@Override
public Http11Request getRequest() {
return new Http11Request(getPath());
}
}
27 changes: 27 additions & 0 deletions tomcat/src/main/java/org/apache/coyote/http11/url/Login.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.apache.coyote.http11.url;

import nextstep.jwp.db.InMemoryUserRepository;
import nextstep.jwp.model.User;
import org.apache.coyote.http11.dto.Http11Request;
import org.apache.coyote.http11.utils.StringParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Login extends Url {
private static final Logger log = LoggerFactory.getLogger(Login.class);

public Login(final String url) {
super(url);
}

@Override
public Http11Request getRequest() {
Http11Request http11Request = StringParser.loginQuery(getPath());

User user = InMemoryUserRepository.findByAccount(http11Request.getAccount())
.orElseThrow(() -> new IllegalArgumentException("아이디 또는 비밀번호를 잘못 입력하였습니다."));
Comment on lines +21 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 이 로직이면 account는 올바른데 password가 틀린 경우에도 정상 동작하는 것으로 보입니다.
User class의 checkPassword()메서드를 이용해 개션해 보면 좋을 것 같습니다👍🏻
ex)http://localhost:8080/login?account=gugu&password=잘못된 패스워드인 경우 -> 정상 동작

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분이 다음 단계 적용으로 알고있어서 적용하다가 뺐었습니다! 다음 단계에서 적용해볼게요~!

log.info("user : {}", user);

return http11Request;
}
}
17 changes: 17 additions & 0 deletions tomcat/src/main/java/org/apache/coyote/http11/url/Url.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.apache.coyote.http11.url;

import org.apache.coyote.http11.dto.Http11Request;

public abstract class Url {
private final String path;

protected Url(String path) {
this.path = path;
}

public abstract Http11Request getRequest();

public String getPath() {
return path;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.apache.coyote.http11.utils;

import org.apache.coyote.http11.dto.Http11Request;

public class StringParser {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클래스 책임을 보니까 StringParser라는 네이밍이 뭔가 추상적인 것 같기도 하네요🧐

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음.. 네이밍은 정말 어렵네요ㅎㅎ ㅠㅠ UrlParser어떤가요?! 확실히 String Parser보단 나은 것 같기도...


private static final String PATH_STANDARD = "?";
private static final String REQUEST_STANDARD = "&";
private static final String DATA_STANDARD = "=";
private static final int VALUE_INDEX = 1;

public static Http11Request loginQuery(String uri) {
int point = uri.indexOf(PATH_STANDARD);
String path = uri.substring(0, point) + ".html";

String queryRequest = uri.substring(point + 1);
if (queryRequest.isEmpty()) {
throw new IllegalArgumentException("요청으로 들어오는 값이 없습니다.");
}
String[] dataMap = queryRequest.split(REQUEST_STANDARD);
String account = dataMap[0].split(DATA_STANDARD)[VALUE_INDEX];
String password = dataMap[1].split(DATA_STANDARD)[VALUE_INDEX];
return new Http11Request(account, password, path);
}

}
Loading