diff --git a/tomcat/build.gradle b/tomcat/build.gradle index 36f1e5d..04cc140 100644 --- a/tomcat/build.gradle +++ b/tomcat/build.gradle @@ -17,8 +17,8 @@ dependencies { implementation 'org.apache.commons:commons-lang3:3.12.0' - implementation 'org.projectlombok:lombok:1.18.22' - annotationProcessor 'org.projectlombok:lombok:1.18.22' + annotationProcessor 'org.projectlombok:lombok:1.18.20' + compileOnly 'org.projectlombok:lombok:1.18.20' testImplementation 'org.assertj:assertj-core:3.22.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' diff --git a/tomcat/src/main/java/nextstep/Application.java b/tomcat/src/main/java/nextstep/Application.java index f3216ae..10beb12 100644 --- a/tomcat/src/main/java/nextstep/Application.java +++ b/tomcat/src/main/java/nextstep/Application.java @@ -1,16 +1,25 @@ package nextstep; import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.http11.scanner.ControllerScanner; +import org.apache.coyote.http11.scanner.MethodScanner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Application { - private static final Logger log = LoggerFactory.getLogger(Application.class); + private static final String BASE_PACKAGE = "org.apache.coyote.http11.controller"; - public static void main(String[] args) { - log.info("web server start."); - final var tomcat = new Tomcat(); - tomcat.start(); - } + private static final Logger log = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + log.info("web server start."); + ControllerScanner controllerScanner = new ControllerScanner(BASE_PACKAGE); + MethodScanner methodScanner = MethodScanner.getInstance(); + methodScanner.scanMethods(controllerScanner.getControllerClasses()); + methodScanner.mapMethodsWithHttpMethod(); + log.info("methodMapByHttpMethod: {}", methodScanner.getMethodMapByHttpMethod()); + final var tomcat = new Tomcat(); + tomcat.start(); + } } 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 d13f01d..50d4800 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,46 +1,47 @@ package org.apache.coyote.http11; -import nextstep.jwp.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.handler.RequestHandler; +import org.apache.coyote.http11.httprequest.HttpRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.net.Socket; +import nextstep.jwp.exception.UncheckedServletException; public class Http11Processor implements Runnable, Processor { - private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + + private final Socket connection; - private final Socket connection; + public Http11Processor(final Socket connection) { + this.connection = connection; + } - public Http11Processor(final Socket connection) { - this.connection = connection; - } + @Override + public void run() { + process(connection); + } - @Override - public void run() { - process(connection); - } + @Override + public void process(final Socket connection) { + try (final var inputStream = connection.getInputStream(); + final var outputStream = connection.getOutputStream()) { - @Override - public void process(final Socket connection) { - try (final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream()) { + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); - final var responseBody = "Hello world!"; + HttpRequest httpRequest = HttpRequest.from(bufferedReader); + RequestHandler requestHandler = RequestHandler.of(httpRequest); - 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); + outputStream.flush(); + } catch (IOException | UncheckedServletException e) { + log.error(e.getMessage(), e); + } + } - outputStream.write(response.getBytes()); - outputStream.flush(); - } catch (IOException | UncheckedServletException e) { - log.error(e.getMessage(), e); - } - } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/annotation/Controller.java b/tomcat/src/main/java/org/apache/coyote/http11/annotation/Controller.java new file mode 100644 index 0000000..a2606f4 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/annotation/Controller.java @@ -0,0 +1,11 @@ +package org.apache.coyote.http11.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Controller { +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/annotation/GetMapping.java b/tomcat/src/main/java/org/apache/coyote/http11/annotation/GetMapping.java new file mode 100644 index 0000000..ba42ac9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/annotation/GetMapping.java @@ -0,0 +1,12 @@ +package org.apache.coyote.http11.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface GetMapping { + String value(); +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/annotation/PostMapping.java b/tomcat/src/main/java/org/apache/coyote/http11/annotation/PostMapping.java new file mode 100644 index 0000000..1582a1b --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/annotation/PostMapping.java @@ -0,0 +1,12 @@ +package org.apache.coyote.http11.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface PostMapping { + String value(); +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/annotation/RequestMapping.java b/tomcat/src/main/java/org/apache/coyote/http11/annotation/RequestMapping.java new file mode 100644 index 0000000..d9c53e5 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/annotation/RequestMapping.java @@ -0,0 +1,12 @@ +package org.apache.coyote.http11.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface RequestMapping { + String value(); +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/ContentType.java b/tomcat/src/main/java/org/apache/coyote/http11/common/ContentType.java new file mode 100644 index 0000000..cf2a164 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/ContentType.java @@ -0,0 +1,33 @@ +package org.apache.coyote.http11.common; + +import java.util.Arrays; + +import lombok.Getter; + +public enum ContentType { + + HTML("html", "text/html;charset=utf-8"), + CSS("css", "text/css;charset=utf-8"), + JS("js", "application/javascript;charset=utf-8"), + PNG("png", "image/png"), + JPG("jpg", "image/jpeg"), + JPEG("jpeg", "image/jpeg"), + GIF("gif", "image/gif"), + DEFAULT("", "text/plain;charset=utf-8"); + + private final String extension; + @Getter + private final String mimeType; + + ContentType(String extension, String mimeType) { + this.extension = extension; + this.mimeType = mimeType; + } + + public static ContentType findByExtension(String extension) { + return Arrays.stream(values()) + .filter(mimeType -> mimeType.extension.equalsIgnoreCase(extension)) + .findFirst() + .orElse(DEFAULT); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpMethod.java new file mode 100644 index 0000000..872fdca --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpMethod.java @@ -0,0 +1,10 @@ +package org.apache.coyote.http11.common; + +public enum HttpMethod { + + GET, + POST, + PUT, + DELETE; + +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpStatus.java new file mode 100644 index 0000000..01c648d --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpStatus.java @@ -0,0 +1,28 @@ +package org.apache.coyote.http11.common; + +public enum HttpStatus { + + OK(200, "OK"), + CREATED(201, "Created"), + FOUND(302, "Found"), + NOT_FOUND(404, "Not Found"), + UNAUTHORIZED(401, "Unauthorized"), + INTERNAL_SERVER_ERROR(500, "Internal Server Erro"), + BAD_REQUEST(400, "Bad Request"); + + private final int code; + private final String status; + + HttpStatus(int code, String status) { + this.code = code; + this.status = status; + } + + public int getCode() { + return code; + } + + public String getStatus() { + return status; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/IndexPageController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/IndexPageController.java new file mode 100644 index 0000000..51da7b5 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/IndexPageController.java @@ -0,0 +1,14 @@ +package org.apache.coyote.http11.controller; + +import org.apache.coyote.http11.annotation.Controller; +import org.apache.coyote.http11.annotation.GetMapping; + +@Controller +public class IndexPageController { + + @GetMapping("/") + public String showIndexPage() { + return "hello world !!"; + } + +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/RequestHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/RequestHandler.java new file mode 100644 index 0000000..9503a59 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/RequestHandler.java @@ -0,0 +1,35 @@ +package org.apache.coyote.http11.handler; + +import java.lang.reflect.Method; +import java.util.Map; + +import org.apache.coyote.http11.common.HttpMethod; +import org.apache.coyote.http11.httprequest.HttpRequest; +import org.apache.coyote.http11.scanner.MethodScanner; + +public class RequestHandler { + + private HttpRequest httpRequest; + private Map> methodMapByHttpMethod; + + private RequestHandler(HttpRequest httpRequest) { + this.httpRequest = httpRequest; + this.methodMapByHttpMethod = MethodScanner.getInstance().getMethodMapByHttpMethod(); + } + + public static RequestHandler of(HttpRequest httpRequest) { + return new RequestHandler(httpRequest); + } + + private Method findMethod(HttpRequest httpRequest) { + Map methodsForHttpMethod = methodMapByHttpMethod.get(httpRequest.getHttpMethod()); + return methodsForHttpMethod.get(httpRequest.getUri()); + } + + private void invokeMethod(Method method, HttpRequest httpRequest) throws Exception { + Class controllerClass = method.getDeclaringClass(); + Object controllerInstance = controllerClass.getDeclaredConstructor().newInstance(); + Object invokedMethodResult = method.invoke(controllerInstance, httpRequest); + + } +} 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 0000000..7c475a5 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httprequest/HttpRequest.java @@ -0,0 +1,94 @@ +package org.apache.coyote.http11.httprequest; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.coyote.http11.common.HttpMethod; + +import lombok.Getter; + +@Getter +public class HttpRequest { + + private static final String CRLF = "\r\n"; + private static final String REGEX_CRLF = "\\r?\\n"; + private static final String BLANK_SPACE = " "; + private static final String CONTENT_LENGTH = "Content-Length"; + private static final String TRANSFER_ENCODING = "Transfer-Encoding"; + private static final String CHUNKED = "chunked"; + private HttpMethod httpMethod; + private String uri; + private String httpVersion; + private Map requestHeaders; + private String requestBody = null; + + private HttpRequest(BufferedReader bufferedReader) throws IOException { + parseRequestLine(bufferedReader); + parseRequestHeaders(bufferedReader); + if (requestHeaders.containsKey(CONTENT_LENGTH)) { + parseContentLengthRequestBody(bufferedReader); + } else if (requestHeaders.containsKey(TRANSFER_ENCODING)) { + parseTransferEncodingRequestBody(bufferedReader); + } + } + + public static HttpRequest from(BufferedReader bufferedReader) throws IOException { + return new HttpRequest(bufferedReader); + } + + private static String getRequestLine(BufferedReader bufferedReader) throws IOException { + return bufferedReader.readLine(); + } + + private void parseRequestLine(BufferedReader bufferedReader) throws IOException { + String requestLine = getRequestLine(bufferedReader); + String[] splittedRequestLine = requestLine.split(BLANK_SPACE); + this.httpMethod = HttpMethod.valueOf(splittedRequestLine[0]); + this.uri = splittedRequestLine[1]; + this.httpVersion = splittedRequestLine[2]; + } + + private static String getRequestHeader(BufferedReader bufferedReader) throws IOException { + StringBuilder requestHeaderBuilder = new StringBuilder(); + String headerLine; + while ((headerLine = bufferedReader.readLine()) != null && !headerLine.isEmpty()) { + requestHeaderBuilder.append(headerLine).append(CRLF); + } + return requestHeaderBuilder.toString(); + } + + private void parseRequestHeaders(BufferedReader bufferedReader) throws IOException { + String requestHeader = getRequestHeader(bufferedReader); + this.requestHeaders = Arrays.stream(requestHeader.split(REGEX_CRLF)) + .filter(line -> line.contains(":")) + .map(line -> line.split(":", 2)) + .collect(Collectors.toMap( + arr -> arr[0].trim(), + arr -> arr[1].trim(), + (value1, value2) -> value1)); + } + + private void parseContentLengthRequestBody(BufferedReader bufferedReader) throws IOException { + int contentLength = Integer.parseInt(requestHeaders.get(CONTENT_LENGTH)); + char[] charRequestBody = new char[contentLength]; + int bytesRead = bufferedReader.read(charRequestBody, 0, contentLength); + this.requestBody = new String(charRequestBody, 0, bytesRead); + } + + private void parseTransferEncodingRequestBody(BufferedReader bufferedReader) throws IOException { + StringBuilder requestBodyBuilder = new StringBuilder(); + String chunkSizeLine; + while (!(chunkSizeLine = bufferedReader.readLine()).equals("0")) { + int chunkSize = Integer.parseInt(chunkSizeLine, 16); + char[] chunkData = new char[chunkSize]; + int bytesRead = bufferedReader.read(chunkData, 0, chunkSize); + requestBodyBuilder.append(chunkData, 0, bytesRead); + bufferedReader.readLine(); + } + bufferedReader.readLine(); + this.requestBody = requestBodyBuilder.toString(); + } +} 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 0000000..9967431 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpresponse/HttpResponse.java @@ -0,0 +1,21 @@ +package org.apache.coyote.http11.httpresponse; + +import java.util.Map; + +import org.apache.coyote.http11.common.HttpStatus; + +import lombok.Getter; + +@Getter +public class HttpResponse { + + private String httpVersion; + private HttpStatus httpStatus; + private Map responseHeaders; + private String responseBody = null; + + public HttpResponse() { + + } + +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java deleted file mode 100644 index a83b5e0..0000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.apache.coyote.http11.request; - -import java.io.BufferedReader; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import lombok.Getter; - -@Getter -public class HttpRequest { - - private String method; - private String path; - private Map headers; - private String body; - - public static HttpRequest parse(BufferedReader bufferedReader) throws IOException { - - HttpRequest request = new HttpRequest(); - - String requestLine = bufferedReader.readLine(); - String[] requestParts = requestLine.split(" "); - request.method = requestParts[0]; - request.path = requestParts[1]; - - String line; - request.headers = new HashMap<>(); - while (!(line = bufferedReader.readLine()).isEmpty()) { - String[] headerParts = line.split(": "); - request.headers.put(headerParts[0], headerParts[1]); - } - - if ("POST".equals(request.method) && request.headers.containsKey("Content-Length")) { - int contentLength = Integer.parseInt(request.headers.get("Content-Length")); - char[] bodyChars = new char[contentLength]; - bufferedReader.read(bodyChars, 0, contentLength); - request.body = new String(bodyChars); - } - - return request; - } - -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/scanner/ControllerScanner.java b/tomcat/src/main/java/org/apache/coyote/http11/scanner/ControllerScanner.java new file mode 100644 index 0000000..680d4f9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/scanner/ControllerScanner.java @@ -0,0 +1,19 @@ +package org.apache.coyote.http11.scanner; + +import java.util.Set; + +import org.apache.coyote.http11.annotation.Controller; +import org.reflections.Reflections; + +public class ControllerScanner { + + private final Reflections reflections; + + public ControllerScanner(String basePackage) { + this.reflections = new Reflections(basePackage); + } + + public Set> getControllerClasses() { + return reflections.getTypesAnnotatedWith(Controller.class); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/scanner/MethodScanner.java b/tomcat/src/main/java/org/apache/coyote/http11/scanner/MethodScanner.java new file mode 100644 index 0000000..b6bb8e8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/scanner/MethodScanner.java @@ -0,0 +1,69 @@ +package org.apache.coyote.http11.scanner; + +import static org.apache.coyote.http11.common.HttpMethod.*; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.apache.coyote.http11.annotation.GetMapping; +import org.apache.coyote.http11.annotation.PostMapping; +import org.apache.coyote.http11.common.HttpMethod; + +import lombok.Getter; + +@Getter +public class MethodScanner { + + private Map uriToGetMethodMap; + private Map uriToPostMethodMap; + private Map> methodMapByHttpMethod; + + private MethodScanner() { + this.uriToGetMethodMap = new HashMap<>(); + this.uriToPostMethodMap = new HashMap<>(); + this.methodMapByHttpMethod = new HashMap<>(); + } + + public static MethodScanner getInstance() { + return LazyHolder.INSTANCE; + } + + private static class LazyHolder { + private static final MethodScanner INSTANCE = new MethodScanner(); + } + + private void scanGetMapping(Set> controllerClasses) { + for (Class controller : controllerClasses) { + for (Method method : controller.getDeclaredMethods()) { + if (method.isAnnotationPresent(GetMapping.class)) { + GetMapping getMapping = method.getAnnotation(GetMapping.class); + this.uriToGetMethodMap.put(getMapping.value(), method); + } + } + } + } + + private void scanPostMapping(Set> controllerClasses) { + for (Class controller : controllerClasses) { + for (Method method : controller.getDeclaredMethods()) { + if (method.isAnnotationPresent(PostMapping.class)) { + PostMapping postMapping = method.getAnnotation(PostMapping.class); + this.uriToPostMethodMap.put(postMapping.value(), method); + } + } + } + } + + public void scanMethods(Set> controllerClasses) { + scanGetMapping(controllerClasses); + scanPostMapping(controllerClasses); + } + + public void mapMethodsWithHttpMethod() { + methodMapByHttpMethod.put(GET, uriToGetMethodMap); + methodMapByHttpMethod.put(POST, uriToPostMethodMap); + } + +}