diff --git a/study/build.gradle b/study/build.gradle index 5c69542f84..87a1f0313c 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -19,7 +19,6 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation 'ch.qos.logback:logback-classic:1.5.7' implementation 'org.apache.commons:commons-lang3:3.14.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.4.1' diff --git a/study/src/main/java/cache/com/example/GreetingController.java b/study/src/main/java/cache/com/example/GreetingController.java index c0053cda42..d6c6c27bdf 100644 --- a/study/src/main/java/cache/com/example/GreetingController.java +++ b/study/src/main/java/cache/com/example/GreetingController.java @@ -1,18 +1,34 @@ package cache.com.example; +import java.io.IOException; +import java.io.OutputStream; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; @Controller public class GreetingController { @GetMapping("/") - public String index() { - return "index"; + public ResponseEntity index() { + StreamingResponseBody stream = new StreamingResponseBody() { + @Override + public void writeTo(OutputStream outputStream) throws IOException { + String htmlContent = "

Hello, World!

"; + outputStream.write(htmlContent.getBytes()); + outputStream.flush(); + } + }; + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML_VALUE) + .body(stream); } /** @@ -25,16 +41,16 @@ public String cacheControl(final HttpServletResponse response) { .cachePrivate() .getHeaderValue(); response.addHeader(HttpHeaders.CACHE_CONTROL, cacheControl); - return "index"; + return "index.html"; } @GetMapping("/etag") public String etag() { - return "index"; + return "index.html"; } @GetMapping("/resource-versioning") public String resourceVersioning() { - return "resource-versioning"; + return "resource-versioning.html"; } } diff --git a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java index 305b1f1e1e..89068404bc 100644 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java @@ -1,13 +1,18 @@ package cache.com.example.cachecontrol; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.mvc.WebContentInterceptor; @Configuration public class CacheWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(final InterceptorRegistry registry) { + WebContentInterceptor interceptor = new WebContentInterceptor(); + interceptor.addCacheMapping(CacheControl.noCache().cachePrivate(), "/"); + registry.addInterceptor(interceptor); } } diff --git a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java index 41ef7a3d9a..21b921e09d 100644 --- a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java +++ b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java @@ -1,12 +1,21 @@ package cache.com.example.etag; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.ShallowEtagHeaderFilter; @Configuration public class EtagFilterConfiguration { -// @Bean -// public FilterRegistrationBean shallowEtagHeaderFilter() { -// return null; -// } + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter(new ShallowEtagHeaderFilter()); + filterRegistrationBean.addUrlPatterns("/*"); // 모든 URL 패턴에 대해 적용 + filterRegistrationBean.setName("etagFilter"); + filterRegistrationBean.setOrder(1); // 필터 순서 설정 + + return filterRegistrationBean; + } } diff --git a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java index 6da6d2c795..fe3489b728 100644 --- a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java +++ b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java @@ -1,7 +1,9 @@ package cache.com.example.version; +import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -20,6 +22,10 @@ public CacheBustingWebConfig(ResourceVersion version) { @Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**") - .addResourceLocations("classpath:/static/"); + .addResourceLocations("classpath:/static/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic()) + .resourceChain(true) + .addResolver(new org.springframework.web.servlet.resource.VersionResourceResolver() + .addContentVersionStrategy("/**")); } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..c152bb20db 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -6,4 +6,9 @@ server: accept-count: 1 max-connections: 1 threads: + min-spare: 2 max: 2 + compression: + enabled: true + mime-types: application/json,application/xml,text/html,text/xml,text/plain + min-response-size: 1024 diff --git a/study/src/main/resources/static/index.html b/study/src/main/resources/static/index.html new file mode 100644 index 0000000000..46cbef0f24 --- /dev/null +++ b/study/src/main/resources/static/index.html @@ -0,0 +1,10 @@ + + + + Getting Started: Serving Web Content + + + +Hello, World! + + diff --git a/study/src/main/resources/static/resource-versioning.html b/study/src/main/resources/static/resource-versioning.html new file mode 100644 index 0000000000..cb8323ebf9 --- /dev/null +++ b/study/src/main/resources/static/resource-versioning.html @@ -0,0 +1,11 @@ + + + + + + Document + + +html > head > script 태그의 src에 version 디렉터리가 생겼다. + + diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..e20b480062 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,13 +1,14 @@ package study; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; /** * 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. @@ -28,7 +29,7 @@ class FileTest { final String fileName = "nextstep.txt"; // todo - final String actual = ""; + final String actual = getClass().getClassLoader().getResource(fileName).getFile(); assertThat(actual).endsWith(fileName); } @@ -44,11 +45,17 @@ class FileTest { final String fileName = "nextstep.txt"; // todo - final Path path = null; - - // todo - final List actual = Collections.emptyList(); - - assertThat(actual).containsOnly("nextstep"); + try { + final Path path = Path.of(getClass().getClassLoader().getResource(fileName).toURI()); + // todo + try { + final List actual = Files.newBufferedReader(path).lines().toList(); + assertThat(actual).containsOnly("nextstep"); + } catch (IOException e) { + + } + } catch (URISyntaxException e) { + + } } } diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index 47a79356b6..13171b221a 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,14 +1,22 @@ package study; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.io.*; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - /** * 자바는 스트림(Stream)으로부터 I/O를 사용한다. * 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다. @@ -53,6 +61,7 @@ class OutputStream_학습_테스트 { * todo * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ + outputStream.write(bytes); final String actual = outputStream.toString(); @@ -78,6 +87,7 @@ class OutputStream_학습_테스트 { * flush를 사용해서 테스트를 통과시킨다. * ByteArrayOutputStream과 어떤 차이가 있을까? */ + outputStream.flush(); verify(outputStream, atLeastOnce()).flush(); outputStream.close(); @@ -96,6 +106,7 @@ class OutputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + outputStream.close(); verify(outputStream, atLeastOnce()).close(); } @@ -128,7 +139,7 @@ class InputStream_학습_테스트 { * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; + final String actual = new String(inputStream.readAllBytes()); assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); @@ -148,6 +159,7 @@ class InputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + inputStream.close(); verify(inputStream, atLeastOnce()).close(); } @@ -169,12 +181,12 @@ class FilterStream_학습_테스트 { * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? */ @Test - void 필터인_BufferedInputStream를_사용해보자() { + void 필터인_BufferedInputStream를_사용해보자() throws IOException { final String text = "필터에 연결해보자."; final InputStream inputStream = new ByteArrayInputStream(text.getBytes()); - final InputStream bufferedInputStream = null; + final InputStream bufferedInputStream = new BufferedInputStream(inputStream); - final byte[] actual = new byte[0]; + final byte[] actual = bufferedInputStream.readAllBytes(); assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class); assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes()); @@ -197,7 +209,7 @@ class InputStreamReader_학습_테스트 { * 필터인 BufferedReader를 사용하면 readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다. */ @Test - void BufferedReader를_사용하여_문자열을_읽어온다() { + void BufferedReader를_사용하여_문자열을_읽어온다() throws IOException { final String emoji = String.join("\r\n", "😀😃😄😁😆😅😂🤣🥲☺️😊", "😇🙂🙃😉😌😍🥰😘😗😙😚", @@ -205,7 +217,7 @@ class InputStreamReader_학습_테스트 { ""); final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes()); - final StringBuilder actual = new StringBuilder(); + final StringBuilder actual = new StringBuilder(new String(inputStream.readAllBytes())); assertThat(actual).hasToString(emoji); } diff --git a/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java b/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java index d3fa57feeb..b041a264ea 100644 --- a/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java +++ b/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java @@ -11,7 +11,7 @@ public class InMemoryUserRepository { private static final Map database = new ConcurrentHashMap<>(); static { - final User user = new User(1L, "gugu", "password", "hkkang@woowahan.com"); + final User user = new User(1L, "gugu", "1", "hkkang@woowahan.com"); database.put(user.getAccount(), user); } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java new file mode 100644 index 0000000000..cd36e4a10c --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java @@ -0,0 +1,32 @@ +package org.apache.coyote.http11; + +import java.util.Arrays; + +public enum ContentType { + + CSS("text/css;charset=utf-8", "css"), + JS("application/javascript;charset=utf-8", "js"), + HTML("text/html;charset=utf-8", "html"), + PNG("image/png", "png"), + JPG("image/jpeg", "jpeg") + ; + + private final String contentType; + private final String extention; + + ContentType(String contentType, String extention) { + this.contentType = contentType; + this.extention = extention; + } + + public String getContentType() { + return contentType; + } + + public static ContentType getContentType(String extention) { + return Arrays.stream(values()) + .filter(contentType1 -> contentType1.extention.equals(extention)) + .findAny() + .orElseThrow(); + } +} 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 bb14184757..d1486f2416 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,13 +1,17 @@ package org.apache.coyote.http11; import com.techcourse.exception.UncheckedServletException; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.Socket; import org.apache.coyote.Processor; +import org.apache.coyote.http11.controller.Controller; +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.httpresponse.HttpResponse; 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); @@ -26,19 +30,20 @@ public void run() { @Override public void process(final Socket connection) { - try (final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream()) { + try ( + final var inputStream = connection.getInputStream(); + final var outputStream = connection.getOutputStream() + ) { + var bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + HttpRequest httpRequest = HttpRequestConvertor.convertHttpRequest(bufferedReader); + + RequestMapping requestMapping = new RequestMapping(); - final var responseBody = "Hello world!"; + Controller controller = requestMapping.getController(httpRequest.getPath()); - 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); + HttpResponse httpResponse = controller.service(httpRequest); - outputStream.write(response.getBytes()); + outputStream.write(httpResponse.getBytes()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java new file mode 100644 index 0000000000..4cb9976c42 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11; + +import java.util.Arrays; + +public enum HttpMethod { + + GET("GET"), + POST("POST") + ; + + private final String name; + + HttpMethod(String name) { + this.name = name; + } + + public static HttpMethod getHttpMethod(String name) { + return Arrays.stream(values()) + .filter(httpMethod -> httpMethod.name.equals(name)) + .findAny() + .orElseThrow(); + } + + public boolean isMethod(String name) { + return this.name.equals(name); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestConvertor.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestConvertor.java new file mode 100644 index 0000000000..cd992e7d10 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestConvertor.java @@ -0,0 +1,69 @@ +package org.apache.coyote.http11; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.httprequest.HttpRequestBody; +import org.apache.coyote.http11.httprequest.HttpRequestHeader; +import org.apache.coyote.http11.httprequest.HttpRequestLine; + +public class HttpRequestConvertor { + + public static HttpRequest convertHttpRequest(BufferedReader bufferedReader) { + try { + String requestLine = bufferedReader.readLine(); + if (requestLine == null) { + throw new RuntimeException("요청이 비어 있습니다."); + } + + HttpRequestLine httpRequestLine = new HttpRequestLine(requestLine); + + Map headers = getHeaders(bufferedReader); + + HttpRequestHeader httpRequestHeader = new HttpRequestHeader(headers); + + if (httpRequestHeader.containsKey("Content-Length")) { + HttpRequestBody httpRequestBody = getHttpRequestBody(bufferedReader, httpRequestHeader); + + return new HttpRequest(httpRequestLine, httpRequestHeader, httpRequestBody); + } + + return new HttpRequest(httpRequestLine, httpRequestHeader); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static HttpRequestBody getHttpRequestBody( + BufferedReader bufferedReader, + HttpRequestHeader httpRequestHeader + ) throws IOException { + int contentLength = Integer.parseInt(httpRequestHeader.getValue("Content-Length")); + char[] buffer = new char[contentLength]; + bufferedReader.read(buffer, 0, contentLength); + String body = new String(buffer); + return new HttpRequestBody(body); + } + + private static Map getHeaders(BufferedReader bufferedReader) throws IOException { + String line; + Map headers = new HashMap<>(); + while ((line = bufferedReader.readLine()) != null && !line.isEmpty()) { + String[] requestLine = line.split(":"); + headers.put(requestLine[0], parseHeaderValue(requestLine)); + } + + return headers; + } + + private static String parseHeaderValue(String[] requestLine) { + StringBuilder sb = new StringBuilder(); + for (int i = 1; i < requestLine.length; i++) { + sb.append(requestLine[i].strip()); + } + + return sb.toString(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpStatusCode.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpStatusCode.java new file mode 100644 index 0000000000..a51eac9edf --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpStatusCode.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11; + +public enum HttpStatusCode { + + OK(200, "OK"), + FOUND(302, "Found") + ; + + private final int code; + private final String message; + + HttpStatusCode(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java b/tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java new file mode 100644 index 0000000000..fa012d5ca8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11; + +import java.util.HashMap; +import java.util.Map; +import org.apache.coyote.http11.controller.Controller; +import org.apache.coyote.http11.controller.LoginController; +import org.apache.coyote.http11.controller.PageController; +import org.apache.coyote.http11.controller.RegisterController; + +public class RequestMapping { + + private final Map controllers = new HashMap<>(); + + public RequestMapping() { + controllers.put("/login", new LoginController()); + controllers.put("/register", new RegisterController()); + controllers.put("page", new PageController()); + } + + public Controller getController(String path) { + if (controllers.containsKey(path)) { + return controllers.get(path); + } + + return new PageController(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Session.java b/tomcat/src/main/java/org/apache/coyote/http11/Session.java new file mode 100644 index 0000000000..424b9cf6f9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Session.java @@ -0,0 +1,36 @@ +package org.apache.coyote.http11; + +import com.techcourse.model.User; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class Session { + + private static final Session INSTANCE = new Session(); + private final Map userMap; + + private Session() { + this.userMap = new HashMap<>(); + } + + public static Session getInstance() { + return INSTANCE; + } + + public void save(String uuid, User user) { + userMap.put(uuid, user); + } + + public boolean containsUser(String uuid) { + return userMap.containsKey(uuid); + } + + public User getUser(String uuid) { + return userMap.get(uuid); + } + + public Set getKeySet() { + return userMap.keySet(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/AbstractController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/AbstractController.java new file mode 100644 index 0000000000..e52a2508da --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/AbstractController.java @@ -0,0 +1,22 @@ +package org.apache.coyote.http11.controller; + +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.httpresponse.HttpResponse; + +public abstract class AbstractController implements Controller { + + @Override + public HttpResponse service(HttpRequest httpRequest) { + if (httpRequest.isMethod("GET")) { + return doGet(httpRequest); + } else if (httpRequest.isMethod("POST")) { + return doPost(httpRequest); + } + + throw new RuntimeException(); + } + + abstract protected HttpResponse doPost(HttpRequest httpRequest); + + abstract protected HttpResponse doGet(HttpRequest httpRequest); +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/Controller.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/Controller.java new file mode 100644 index 0000000000..3f2f18697a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/Controller.java @@ -0,0 +1,9 @@ +package org.apache.coyote.http11.controller; + +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.httpresponse.HttpResponse; + +public interface Controller { + + HttpResponse service(HttpRequest httpRequest); +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/LoginController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/LoginController.java new file mode 100644 index 0000000000..62d0b29b6f --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/LoginController.java @@ -0,0 +1,91 @@ +package org.apache.coyote.http11.controller; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; +import org.apache.coyote.http11.ContentType; +import org.apache.coyote.http11.HttpStatusCode; +import org.apache.coyote.http11.Session; +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.httpresponse.HttpResponse; +import org.apache.coyote.http11.httpresponse.HttpResponseBody; +import org.apache.coyote.http11.httpresponse.HttpResponseHeader; +import org.apache.coyote.http11.httpresponse.HttpStatusLine; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LoginController extends AbstractController { + + private static final Logger log = LoggerFactory.getLogger(LoginController.class); + + private final Session session = Session.getInstance(); + + @Override + protected HttpResponse doPost(HttpRequest httpRequest) { + String requestBody = httpRequest.getBody(); + String[] token = requestBody.split("&"); + String account = token[0].split("=")[1]; + String password = token[1].split("=")[1]; + + User user = InMemoryUserRepository.findByAccount(account) + .orElseThrow(); + UUID uuid = UUID.randomUUID(); + + HttpStatusLine httpStatusLine = new HttpStatusLine(httpRequest.getVersion(), HttpStatusCode.FOUND); + HttpResponseHeader httpResponseHeader = new HttpResponseHeader(); + if (user.checkPassword(password)) { + session.save(uuid.toString(), user); + httpResponseHeader.addHeaders("Set-Cookie", "JSESSIONID=" + uuid); + httpResponseHeader.addHeaders("Location", "/index.html"); + log.info(user.toString()); + } else { + httpResponseHeader.addHeaders("Location", "/401.html"); + log.error("비밀번호 불일치"); + } + + return new HttpResponse(httpStatusLine, httpResponseHeader); + } + + @Override + protected HttpResponse doGet(HttpRequest httpRequest) { + try { + HttpStatusLine httpStatusLine = new HttpStatusLine(httpRequest.getVersion(), HttpStatusCode.OK); + + if (httpRequest.containsKey("Cookie")) { + String[] cookies = httpRequest.getValue("Cookie").split("; "); + String cookie = ""; + for (String c : cookies) { + if (c.contains("JSESSIONID")) { + cookie = c.split("=")[1]; + } + } + if (session.containsUser(cookie)) { + User user = session.getUser(cookie); + log.info(user.toString()); + httpStatusLine = new HttpStatusLine(httpStatusLine.getVersion(), HttpStatusCode.FOUND); + HttpResponseHeader httpResponseHeader = new HttpResponseHeader(); + httpResponseHeader.addHeaders("Set-Cookie", "JSESSIONID=" + cookie); + httpResponseHeader.addHeaders("Location", "/index.html"); + return new HttpResponse(httpStatusLine, httpResponseHeader); + } + } + + String fileName = "static/login.html"; + var resourceUrl = getClass().getClassLoader().getResource(fileName); + Path filePath = Path.of(resourceUrl.toURI()); + String responseBody = new String(Files.readAllBytes(filePath)); + HttpResponseHeader httpResponseHeader = new HttpResponseHeader(); + httpResponseHeader.addHeaders("Content-Type", ContentType.HTML.getContentType()); + httpResponseHeader.addHeaders("Content-Length", String.valueOf(responseBody.getBytes().length)); + HttpResponseBody httpResponseBody = new HttpResponseBody(responseBody); + + return new HttpResponse(httpStatusLine, httpResponseHeader, httpResponseBody); + } catch (URISyntaxException | IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/PageController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/PageController.java new file mode 100644 index 0000000000..cda0b2f59a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/PageController.java @@ -0,0 +1,45 @@ +package org.apache.coyote.http11.controller; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.coyote.http11.ContentType; +import org.apache.coyote.http11.HttpStatusCode; +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.httpresponse.HttpResponse; +import org.apache.coyote.http11.httpresponse.HttpResponseBody; +import org.apache.coyote.http11.httpresponse.HttpResponseHeader; +import org.apache.coyote.http11.httpresponse.HttpStatusLine; + +public class PageController extends AbstractController { + + @Override + protected HttpResponse doPost(HttpRequest httpRequest) { + return null; + } + + @Override + protected HttpResponse doGet(HttpRequest httpRequest) { + try { + HttpStatusLine httpStatusLine = new HttpStatusLine(httpRequest.getVersion(), HttpStatusCode.OK); + + String path = httpRequest.getPath(); + if (!httpRequest.getPath().contains(".")) { + path += ".html"; + } + String fileName = "static" + path; + var resourceUrl = getClass().getClassLoader().getResource(fileName); + Path filePath = Path.of(resourceUrl.toURI()); + String responseBody = new String(Files.readAllBytes(filePath)); + HttpResponseHeader httpResponseHeader = new HttpResponseHeader(); + httpResponseHeader.addHeaders("Content-Type", ContentType.getContentType(path.split("\\.")[1]).getContentType()); + httpResponseHeader.addHeaders("Content-Length", String.valueOf(responseBody.getBytes().length)); + HttpResponseBody httpResponseBody = new HttpResponseBody(responseBody); + + return new HttpResponse(httpStatusLine, httpResponseHeader, httpResponseBody); + } catch (URISyntaxException | IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/RegisterController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/RegisterController.java new file mode 100644 index 0000000000..736aa4d852 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/RegisterController.java @@ -0,0 +1,55 @@ +package org.apache.coyote.http11.controller; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.coyote.http11.ContentType; +import org.apache.coyote.http11.HttpStatusCode; +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.httpresponse.HttpResponse; +import org.apache.coyote.http11.httpresponse.HttpResponseBody; +import org.apache.coyote.http11.httpresponse.HttpResponseHeader; +import org.apache.coyote.http11.httpresponse.HttpStatusLine; + +public class RegisterController extends AbstractController { + + @Override + protected HttpResponse doPost(HttpRequest httpRequest) { + String requestBody = httpRequest.getBody(); + String[] token = requestBody.split("&"); + String account = token[0].split("=")[1]; + String email = token[1].split("=")[1]; + String password = token[2].split("=")[1]; + User user = new User(account, password, email); + InMemoryUserRepository.save(user); + + HttpStatusLine httpStatusLine = new HttpStatusLine(httpRequest.getVersion(), HttpStatusCode.FOUND); + HttpResponseHeader httpResponseHeader = new HttpResponseHeader(); + httpResponseHeader.addHeaders("Location", "/index.html"); + + return new HttpResponse(httpStatusLine, httpResponseHeader); + } + + @Override + protected HttpResponse doGet(HttpRequest httpRequest) { + try { + HttpStatusLine httpStatusLine = new HttpStatusLine(httpRequest.getVersion(), HttpStatusCode.OK); + + String fileName = "static/register.html"; + var resourceUrl = getClass().getClassLoader().getResource(fileName); + Path filePath = Path.of(resourceUrl.toURI()); + String responseBody = new String(Files.readAllBytes(filePath)); + HttpResponseHeader httpResponseHeader = new HttpResponseHeader(); + httpResponseHeader.addHeaders("Content-Type", ContentType.HTML.getContentType()); + httpResponseHeader.addHeaders("Content-Length", String.valueOf(responseBody.getBytes().length)); + HttpResponseBody httpResponseBody = new HttpResponseBody(responseBody); + + return new HttpResponse(httpStatusLine, httpResponseHeader, httpResponseBody); + } catch (URISyntaxException | IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequest.java new file mode 100644 index 0000000000..1e614078b8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequest.java @@ -0,0 +1,68 @@ +package org.apache.coyote.http11.httprequest; + +import org.apache.coyote.http11.HttpMethod; + +public class HttpRequest { + + private final HttpRequestLine httpRequestLine; + private final HttpRequestHeader httpRequestHeader; + private final HttpRequestBody httpRequestBody; + + public HttpRequest(HttpRequestLine httpRequestLine, HttpRequestHeader httpRequestHeader, HttpRequestBody httpRequestBody) { + this.httpRequestLine = httpRequestLine; + this.httpRequestHeader = httpRequestHeader; + this.httpRequestBody = httpRequestBody; + } + + public HttpRequest(HttpRequestLine httpRequestLine, HttpRequestHeader httpRequestHeader) { + this(httpRequestLine, httpRequestHeader, null); + } + + public boolean isMethod(String name) { + return httpRequestLine.isMethod(name); + } + + public boolean isPath(String path) { + return httpRequestLine.isPath(path); + } + + public boolean containsKey(String key) { + return httpRequestHeader.containsKey(key); + } + + public String getValue(String key) { + return httpRequestHeader.getValue(key); + } + + public HttpMethod getMethod() { + return httpRequestLine.getMethod(); + } + + public String getPath() { + return httpRequestLine.getPath(); + } + + public String getVersion() { + return httpRequestLine.getVersion(); + } + + public String getBody() { + return httpRequestBody.getBody(); + } + + public HttpRequestHeader getHttpRequestHeader() { + return httpRequestHeader; + } + + public HttpRequestBody getHttpRequestBody() { + return httpRequestBody; + } + + @Override + public String toString() { + return "HttpRequest{\n" + + "httpRequestHeader=" + httpRequestHeader + + ",\n httpRequestBody=" + httpRequestBody + + "\n}"; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestBody.java b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestBody.java new file mode 100644 index 0000000000..6c303051c2 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestBody.java @@ -0,0 +1,21 @@ +package org.apache.coyote.http11.httprequest; + +public class HttpRequestBody { + + private final String body; + + public HttpRequestBody(String body) { + this.body = body; + } + + public String getBody() { + return body; + } + + @Override + public String toString() { + return "HttpRequestBody{\n" + + "body='" + body + '\'' + + "\n}"; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestHeader.java new file mode 100644 index 0000000000..bf29b0cdc4 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestHeader.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11.httprequest; + +import java.util.Map; + +public class HttpRequestHeader { + + private final Map headers; + + public HttpRequestHeader(Map headers) { + this.headers = headers; + } + + public boolean containsKey(String key) { + return headers.containsKey(key); + } + + public String getValue(String key) { + return headers.get(key); + } + + @Override + public String toString() { + return "HttpRequestHeader{" + + ", \nheaders=" + headers + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestLine.java new file mode 100644 index 0000000000..6befc619d4 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequestLine.java @@ -0,0 +1,37 @@ +package org.apache.coyote.http11.httprequest; + +import org.apache.coyote.http11.HttpMethod; + +public class HttpRequestLine { + + private final HttpMethod method; + private final String path; + private final String version; + + public HttpRequestLine(String requestLine) { + String[] headerFirstLine = requestLine.split(" "); + this.method = HttpMethod.getHttpMethod(headerFirstLine[0]); + this.path = headerFirstLine[1]; + this.version = headerFirstLine[2]; + } + + public boolean isMethod(String name) { + return method.isMethod(name); + } + + public boolean isPath(String path) { + return this.path.equals(path); + } + + public HttpMethod getMethod() { + return method; + } + + public String getPath() { + return path; + } + + public String getVersion() { + return version; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponse.java new file mode 100644 index 0000000000..2960b502c7 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponse.java @@ -0,0 +1,65 @@ +package org.apache.coyote.http11.httpresponse; + +import java.util.Map; + +public class HttpResponse { + + private final HttpStatusLine httpStatusLine; + private final HttpResponseHeader httpResponseHeader; + private final HttpResponseBody httpResponseBody; + + public HttpResponse( + HttpStatusLine httpStatusLine, + HttpResponseHeader httpResponseHeader, + HttpResponseBody httpResponseBody + ) { + this.httpStatusLine = httpStatusLine; + this.httpResponseHeader = httpResponseHeader; + this.httpResponseBody = httpResponseBody; + } + + public HttpResponse(HttpStatusLine httpStatusLine, HttpResponseHeader httpResponseHeader) { + this(httpStatusLine, httpResponseHeader, null); + } + + public byte[] getBytes() { + String statusLine = httpStatusLine.getVersion() + " " + httpStatusLine.getHttpStatusCode().getCode() + " " + + httpStatusLine.getHttpStatusCode().getMessage(); + Map headers = httpResponseHeader.getHeaders(); + StringBuilder sb = new StringBuilder(); + int size = headers.keySet().size(); + int i = 1; + for (String key : headers.keySet()) { + if (i < size) { + sb.append(key).append(": ").append(headers.get(key)).append(" \r\n"); + size++; + } else { + sb.append(key).append(": ").append(headers.get(key)); + } + } + if (httpResponseBody != null) { + String responseBody = httpResponseBody.getBody(); + String join = String.join("\r\n", + statusLine, + sb.toString(), + responseBody); + return join.getBytes(); + } + String join = String.join("\r\n", + statusLine, + sb.toString()); + return join.getBytes(); + } + + public HttpStatusLine getHttpStatusLine() { + return httpStatusLine; + } + + public HttpResponseHeader getHttpResponseHeader() { + return httpResponseHeader; + } + + public HttpResponseBody getHttpResponseBody() { + return httpResponseBody; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponseBody.java b/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponseBody.java new file mode 100644 index 0000000000..4c4a5cb111 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponseBody.java @@ -0,0 +1,14 @@ +package org.apache.coyote.http11.httpresponse; + +public class HttpResponseBody { + + private final String body; + + public HttpResponseBody(String body) { + this.body = body; + } + + public String getBody() { + return body; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponseHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponseHeader.java new file mode 100644 index 0000000000..e350a1b84a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponseHeader.java @@ -0,0 +1,21 @@ +package org.apache.coyote.http11.httpresponse; + +import java.util.HashMap; +import java.util.Map; + +public class HttpResponseHeader { + + private final Map headers; + + public HttpResponseHeader() { + this.headers = new HashMap<>(); + } + + public void addHeaders(String key, String value) { + headers.put(key, value); + } + + public Map getHeaders() { + return headers; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpStatusLine.java b/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpStatusLine.java new file mode 100644 index 0000000000..4eaf9eba69 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpStatusLine.java @@ -0,0 +1,22 @@ +package org.apache.coyote.http11.httpresponse; + +import org.apache.coyote.http11.HttpStatusCode; + +public class HttpStatusLine { + + private final String version; + private final HttpStatusCode httpStatusCode; + + public HttpStatusLine(String version, HttpStatusCode httpStatusCode) { + this.version = version; + this.httpStatusCode = httpStatusCode; + } + + public String getVersion() { + return version; + } + + public HttpStatusCode getHttpStatusCode() { + return httpStatusCode; + } +} diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..bc933357f2 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -20,7 +20,7 @@

로그인

-
+
diff --git a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java index 2aba8c56e0..9d9071b6f9 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -1,14 +1,13 @@ package org.apache.coyote.http11; -import org.junit.jupiter.api.Test; -import support.StubSocket; +import static org.assertj.core.api.Assertions.assertThat; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; +import support.StubSocket; class Http11ProcessorTest { @@ -52,7 +51,7 @@ void index() throws IOException { final URL resource = getClass().getClassLoader().getResource("static/index.html"); var expected = "HTTP/1.1 200 OK \r\n" + "Content-Type: text/html;charset=utf-8 \r\n" + - "Content-Length: 5564 \r\n" + + "Content-Length: 5670 \r\n" + "\r\n"+ new String(Files.readAllBytes(new File(resource.getFile()).toPath()));