All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sam.server.http.web.HttpResponse Maven / Gradle / Ivy

package org.sam.server.http.web;

import org.sam.server.common.ServerProperties;
import org.sam.server.constant.ContentType;
import org.sam.server.constant.HttpMethod;
import org.sam.server.constant.HttpStatus;
import org.sam.server.exception.ResourcesNotFoundException;
import org.sam.server.http.Cookie;
import org.sam.server.http.CookieStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.*;

/**
 * 요청을 해석하고 응답하는 클래스입니다. 정적 자원을 반환합니다.
 *
 * @author hypernova1
 * @see #execute(String, HttpStatus) 
 */
public class HttpResponse implements Response {

    private static final Logger logger = LoggerFactory.getLogger(HttpResponse.class);

    private final PrintWriter writer;

    private final BufferedOutputStream outputStream;

    private final Map headers = new HashMap<>();

    private final Set cookies = CookieStore.getCookies();

    private final String requestPath;

    private final HttpMethod requestMethod;

    private final Set allowedMethods = new LinkedHashSet<>();

    private String filePath;

    private HttpStatus httpStatus;

    private String contentMimeType;

    private long fileLength;

    private HttpResponse(OutputStream os, String path, HttpMethod requestMethod) {
        int bufferSize = BUFFER_SIZE_PROPERTY != null ? Integer.parseInt(BUFFER_SIZE_PROPERTY) : 8192;
        this.writer = new PrintWriter(os);
        this.outputStream = new BufferedOutputStream(os, bufferSize);
        this.requestPath = path;
        this.requestMethod = requestMethod;
    }

    /**
     * 인스턴스를 생성합니다.
     *
     * @param os 응답을 출력할 스트림
     * @param requestPath 요청 URL
     * @param requestMethod 요청 HTTP Method
     * @return HttpResponse 인스턴스
     * */
    public static Response of(OutputStream os, String requestPath, HttpMethod requestMethod) {
        return new HttpResponse(os, requestPath, requestMethod);
    }

    @Override
    public void execute(String pathOrJson, HttpStatus status) {
        this.httpStatus = status;
        try {
            if (getContentMimeType().equals(ContentType.APPLICATION_JSON) && !requestMethod.equals(HttpMethod.OPTIONS)) {
                this.fileLength = readJson(pathOrJson);
            } else if (allowedMethods.isEmpty()) {
                this.fileLength = readStaticResource(pathOrJson);
            }
            setHeaders();
            printHeaders();
            CookieStore.vacateList();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            writer.flush();
            try {
                outputStream.flush();
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            writer.close();
        }
    }

    /**
     * 정적 자원의 경로를 받아 파일을 읽습니다. 파일이 존재하지 않으면 notFound 메서드를 호출합니다.
     *
     * @param filePath 파일 경로
     * @return 파일의 길이
     * @see #notFound()
     * @see #readFileData(File)
     * @see #readStaticResources(InputStream)
     * */
    private long readStaticResource(String filePath) {
        InputStream fis = Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath);
        File staticFile = new File("src/main" + filePath);
        if (fis == null && !staticFile.exists()) {
            notFound();
            return 0;
        }
        if (!filePath.equals(NOT_FOUND_PAGE) && requestMethod.equals(HttpMethod.OPTIONS)) {
            allowedMethods.add(HttpMethod.GET);
            return 0;
        }
        long fileLength = 0;
        try {
            if (staticFile.exists()) {
                fileLength = readFileData(staticFile);
            } else {
                assert fis != null;
                fileLength = readStaticResources(fis);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return fileLength;
    }

    /**
     * 정적 파일을 읽은 후 OutputStream에 쓰고 파일의 길이를 반환합니다.
     *
     * @param fis 파일을 읽은 스트림
     * @return 파일의 길이
     * @throws IOException 파일을 읽다가 오류 발생시
     * */
    private long readStaticResources(InputStream fis) throws IOException {
        long fileLength = 0;
        int i;
        while ((i = fis.read()) != -1) {
            if (!this.requestMethod.equals(HttpMethod.HEAD)) {
                outputStream.write(i);
            }
            fileLength++;
        }
        return fileLength;
    }

    /**
     * 정적 파일을 읽은 후 OutputStream에 쓰고 파일의 길이를 반환합니.
     *
     * @param file 정적 파일
     * @return 파일의 길이
     * @throws IOException 파일을 읽다가 문제 발생시
     * */
    private long readFileData(File file) throws IOException {
        FileInputStream fis = new FileInputStream(file);
        if (!this.requestMethod.equals(HttpMethod.HEAD)) {
            int len;
            byte[] buf = new byte[fis.available()];
            while ((len = fis.read(buf)) > 0) {
                outputStream.write(buf, 0, len);
            }
        }
        fis.close();

        return file.length();
    }

    /**
     * JSON 문자열을 OutputStream에 쓰고 바이트 길이를 반환합니다.
     *
     * @param json JSON 문자열
     * @return JSON 문자열의 바이트 길이
     * @throws IOException 문자열을 읽다가 오류 발생시
     * */
    private int readJson(String json) throws IOException {
        if (httpStatus.equals(HttpStatus.NOT_FOUND) || httpStatus.equals(HttpStatus.BAD_REQUEST)) {
            return 0;
        }

        byte[] bytes = json.getBytes(StandardCharsets.UTF_8);

        if (!this.requestMethod.equals(HttpMethod.HEAD)) {
            outputStream.write(bytes);
        }

        return bytes.length;
    }

    /**
     * 응답 헤더를 OutputStream에 씁니다.
     * */
    private void printHeaders() {


        writer.print("HTTP/1.1 " + httpStatus.getCode() + " " + httpStatus.getMessage() + "\r\n");
        for (String key : headers.keySet()) {
            writer.print(key + ": " + headers.get(key) + "\r\n");
        }
        printCookies();
        writer.print("\r\n");
    }

    /**
     * 응답할 헤더 리스트를 세팅합니다.
     * */
    private void setHeaders() {
        headers.put("Server", "Java HTTP Server from sam : 1.0");
        headers.put("Date", LocalDateTime.now());
        headers.put("Content-Type", getContentMimeType().getValue());
        headers.put("Content-length", this.fileLength);
        headers.put("Accept-Ranges", "bytes");
        headers.put("Connection", "Keep-Alive");
        headers.put("Keep-Alive", "timeout=60");

        if (requestPath.startsWith("/resources")) {
            headers.put("Cache-Control", "max-age=86400");
        } else {
            headers.put("Cache-Control", "no-cache, no-store, must-revalidate");
        }

        if (requestMethod.equals(HttpMethod.OPTIONS) && allowedMethods.size() > 0) {
            StringJoiner stringJoiner = new StringJoiner(", ");
            for (HttpMethod allowedMethod : allowedMethods) {
                stringJoiner.add(allowedMethod.toString());
            }
            headers.put("Allow", stringJoiner.toString());
        }
    }

    /**
     * 조건에 따라 미디어타입을 반환합니다.
     *
     * @return 미디어 타입
     * @see org.sam.server.constant.ContentType
     * @see org.sam.server.constant.HttpStatus
     * */
    private ContentType getContentMimeType() {
        if (contentMimeType != null) return ContentType.get(contentMimeType);
        if (isHtmlResponse()) return ContentType.TEXT_HTML;
        if (requestPath.endsWith(".css")) return ContentType.CSS;
        if (requestPath.endsWith(".js")) return ContentType.JAVASCRIPT;

        return ContentType.TEXT_PLAIN;
    }

    /**
     * 쿠키에 대한 정보를 OutputStream에 씁니다.
     *
     * @see org.sam.server.http.Cookie
     * */
    private void printCookies() {
        for (Cookie cookie : cookies) {
            StringBuilder line = new StringBuilder();
            line.append("Set-Cookie: ");
            line.append(cookie.getName()).append("=").append(cookie.getValue());
            if (cookie.getMaxAge() != 0) {
                line.append("; Expires=").append(cookie.getExpires());
                line.append("; Max-Age=").append(cookie.getMaxAge());
            }
            if (ServerProperties.isSSL()) {
                line.append("; Secure");
            }
            if (cookie.isHttpOnly()) {
                line.append("; HttpOnly");
            }
            line.append("; Path=").append(cookie.getPath());
            writer.print(line.toString() + "\r\n");
        }
    }

    @Override
    public void setContentMimeType(ContentType contentMimeType) {
        this.contentMimeType = contentMimeType.getValue();
    }

    @Override
    public void addCookies(Cookie cookie) {
        this.cookies.add(cookie);
    }

    @Override
    public void setHeader(String key, String value) {
        this.headers.put(key, value);
    }

    @Override
    public Object getHeader(String key) {
        return headers.get(key);
    }

    @Override
    public Set getHeaderNames() {
        return headers.keySet();
    }

    @Override
    public void staticResources() {
        String filePath = requestPath.replace("/resources", "/resources/static");
        execute(filePath, HttpStatus.OK);
    }

    @Override
    public void notFound() {
        logger.warn("File " + requestPath + " not found");
        execute(NOT_FOUND_PAGE, HttpStatus.NOT_FOUND);
    }

    @Override
    public void badRequest() {
        logger.warn("Bad Request");
        execute(BAD_REQUEST_PAGE, HttpStatus.BAD_REQUEST);
    }

    @Override
    public void methodNotAllowed() {
        logger.warn("Method Not Allowed");
        execute(METHOD_NOT_ALLOWED_PAGE, HttpStatus.METHOD_NOT_ALLOWED);
    }

    @Override
    public void indexFile() {
        if (this.requestPath.endsWith("/"))
            filePath = DEFAULT_FILE_PAGE;
        this.contentMimeType = ContentType.TEXT_HTML.getValue();
        execute(filePath, HttpStatus.OK);
    }

    @Override
    public void favicon() throws ResourcesNotFoundException {
        filePath = FAVICON;
        this.contentMimeType = ContentType.X_ICON.getValue();
        execute(filePath, HttpStatus.OK);
    }

    @Override
    public void addAllowedMethod(HttpMethod httpMethod) {
        this.allowedMethods.add(httpMethod);
    }

    @Override
    public void allowedMethods() {
        if (allowedMethods.isEmpty()) {
            this.notFound();
            return;
        }
        this.execute(null, HttpStatus.OK);
    }

    /**
     * 응답할 MIME 형식이 HTML인지 확인합니다
     *
     * @return HTML 여부
     * */
    private boolean isHtmlResponse() {
        return httpStatus.equals(HttpStatus.NOT_FOUND) || httpStatus.equals(HttpStatus.BAD_REQUEST) ||
                httpStatus.equals(HttpStatus.NOT_IMPLEMENTED) || this.requestPath.endsWith(".html");
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy