diff --git a/study/build.gradle b/study/build.gradle index 87a1f0313c..b8f82f0e57 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -21,7 +21,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' 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' + implementation 'com.github.jknack:handlebars-springmvc:4.4.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.assertj:assertj-core:3.26.0' 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..bfda1a9029 100644 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java @@ -1,6 +1,10 @@ package cache.com.example.cachecontrol; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -9,5 +13,18 @@ public class CacheWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(final InterceptorRegistry registry) { + registry.addInterceptor(new CacheControlInterceptor()) + .addPathPatterns("/"); } } + +class CacheControlInterceptor implements HandlerInterceptor { + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) throws Exception { + HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); + response.setHeader("Cache-Control", "no-cache, private"); + } +} + 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..d4498c5fa0 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,19 @@ 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() { + ShallowEtagHeaderFilter filter = new ShallowEtagHeaderFilter(); + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(filter); + filterRegistrationBean.addUrlPatterns("/etag"); + filterRegistrationBean.addUrlPatterns("/resources/*"); + 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..c7addcdab4 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.time.Duration; 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,7 @@ 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(Duration.ofDays(365)).cachePublic()); } } diff --git a/study/src/main/java/cache/com/example/version/HandlebarsConfig.java b/study/src/main/java/cache/com/example/version/HandlebarsConfig.java new file mode 100644 index 0000000000..3c22a4fdda --- /dev/null +++ b/study/src/main/java/cache/com/example/version/HandlebarsConfig.java @@ -0,0 +1,27 @@ +package cache.com.example.version; + +import com.github.jknack.handlebars.springmvc.HandlebarsViewResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.ViewResolver; + +@Configuration +public class HandlebarsConfig { + + private final VersionHandlebarsHelper versionHandlebarsHelper; + + public HandlebarsConfig(VersionHandlebarsHelper versionHandlebarsHelper) { + this.versionHandlebarsHelper = versionHandlebarsHelper; + } + + @Bean + public ViewResolver handlebarsViewResolver() { + HandlebarsViewResolver viewResolver = new HandlebarsViewResolver(); + viewResolver.registerHelper("staticUrls", versionHandlebarsHelper); + + viewResolver.setPrefix("classpath:/templates/"); + viewResolver.setSuffix(".html"); + + return viewResolver; + } +} diff --git a/study/src/main/java/cache/com/example/version/VersionHandlebarsHelper.java b/study/src/main/java/cache/com/example/version/VersionHandlebarsHelper.java index a8e004466a..8bf617189f 100644 --- a/study/src/main/java/cache/com/example/version/VersionHandlebarsHelper.java +++ b/study/src/main/java/cache/com/example/version/VersionHandlebarsHelper.java @@ -1,13 +1,14 @@ package cache.com.example.version; +import com.github.jknack.handlebars.Helper; import com.github.jknack.handlebars.Options; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import pl.allegro.tech.boot.autoconfigure.handlebars.HandlebarsHelper; +import org.springframework.stereotype.Component; -@HandlebarsHelper -public class VersionHandlebarsHelper { +@Component +public class VersionHandlebarsHelper implements Helper { private static final Logger log = LoggerFactory.getLogger(VersionHandlebarsHelper.class); @@ -22,4 +23,9 @@ public String staticUrls(String path, Options options) { log.debug("static url : {}", path); return String.format("/resources/%s%s", version.getVersion(), path); } + + @Override + public Object apply(Object context, Options options) { + return staticUrls(context.toString(), options); + } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index e3503a5fb9..7e59fe2646 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -1,7 +1,7 @@ -handlebars: - suffix: .html - server: + compression: + enabled: true + min-response-size: 10 tomcat: accept-count: 1 max-connections: 1 diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..b5ef79fcb5 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,53 +1,49 @@ 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.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 java.util.Objects; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; /** - * 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. - * File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다. + * 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다. */ @DisplayName("File 클래스 학습 테스트") class FileTest { /** * resource 디렉터리 경로 찾기 - * - * File 객체를 생성하려면 파일의 경로를 알아야 한다. - * 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. - * resource 디렉터리의 경로는 어떻게 알아낼 수 있을까? + *

+ * File 객체를 생성하려면 파일의 경로를 알아야 한다. 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. resource 디렉터리의 경로는 어떻게 알아낼 수 + * 있을까? */ @Test void resource_디렉터리에_있는_파일의_경로를_찾는다() { final String fileName = "nextstep.txt"; - // todo - final String actual = ""; - + final String actual = Objects.requireNonNull(getClass().getClassLoader().getResource(fileName)).getPath(); +// final String actual = Objects.requireNonNull(getClass().getResource("/" + fileName)).getPath(); // 동일 결과 assertThat(actual).endsWith(fileName); } /** * 파일 내용 읽기 - * - * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. - * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. + *

+ * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws IOException { final String fileName = "nextstep.txt"; - // todo - final Path path = null; + String pathStr = Objects.requireNonNull(getClass().getClassLoader().getResource(fileName)).getPath(); + final Path path = Path.of(pathStr); - // todo - final List actual = Collections.emptyList(); + final List actual = Files.readAllLines(path); assertThat(actual).containsOnly("nextstep"); } diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index 47a79356b6..b9c7ab5e3d 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,101 +1,103 @@ 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.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +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)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다. - * - * InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다. - * FilterStream은 InputStream이나 OutputStream에 연결될 수 있다. - * FilterStream은 읽거나 쓰는 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환) - * - * Stream은 데이터를 바이트로 읽고 쓴다. - * 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다. - * Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 처리할 수 있다. + * 자바는 스트림(Stream)으로부터 I/O를 사용한다. 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다. + *

+ * InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다. FilterStream은 InputStream이나 OutputStream에 연결될 수 있다. FilterStream은 읽거나 쓰는 + * 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환) + *

+ * Stream은 데이터를 바이트로 읽고 쓴다. 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다. Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 + * 처리할 수 있다. */ @DisplayName("Java I/O Stream 클래스 학습 테스트") class IOStreamTest { /** * OutputStream 학습하기 - * - * 자바의 기본 출력 클래스는 java.io.OutputStream이다. - * OutputStream의 write(int b) 메서드는 기반 메서드이다. + *

+ * 자바의 기본 출력 클래스는 java.io.OutputStream이다. OutputStream의 write(int b) 메서드는 기반 메서드이다. * public abstract void write(int b) throws IOException; */ @Nested class OutputStream_학습_테스트 { /** - * OutputStream은 다른 매체에 바이트로 데이터를 쓸 때 사용한다. - * OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 사용한다. - * 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때, - * 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 사용한다. - * + * OutputStream은 다른 매체에 바이트로 데이터를 쓸 때 사용한다. OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 + * 사용한다. 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때, 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 + * 사용한다. + *

* write 메서드는 데이터를 바이트로 출력하기 때문에 비효율적이다. * write(byte[] data)write(byte b[], int off, int len) 메서드는 * 1바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다. */ @Test void OutputStream은_데이터를_바이트로_처리한다() throws IOException { - final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112}; - final OutputStream outputStream = new ByteArrayOutputStream(bytes.length); + final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112}; // n e x t s t e p - /** - * todo - * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 - */ + // 인수 없이도 생성 가능. 기본 사이즈는 32이며, 자동 확장 가능 + final OutputStream outputStream = new ByteArrayOutputStream(bytes.length); + outputStream.write(bytes); final String actual = outputStream.toString(); assertThat(actual).isEqualTo("nextstep"); - outputStream.close(); + outputStream.close(); // ByteArrayOutputStream은 메모리 기반 스트림이므로 close() 메서드를 호출해도 아무런 효과가 없지만, 일관성을 위해 닫아줌 } /** - * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. - * BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. - * - * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. - * flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. - * Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 - * 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다. + * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. + *

+ * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. Stream은 + * 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다. */ @Test void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException { - final OutputStream outputStream = mock(BufferedOutputStream.class); + final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112}; // n e x t s t e p + final OutputStream outputStream = new ByteArrayOutputStream(bytes.length); + final OutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); - /** - * todo - * flush를 사용해서 테스트를 통과시킨다. - * ByteArrayOutputStream과 어떤 차이가 있을까? - */ + bufferedOutputStream.write(bytes); + bufferedOutputStream.flush(); // 기본 버퍼 사이즈는 8192 바이트. 버퍼가 채워지기 전 명시적 flush(). + final String actual = outputStream.toString(); - verify(outputStream, atLeastOnce()).flush(); + assertThat(actual).isEqualTo("nextstep"); outputStream.close(); } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void OutputStream은_사용하고_나서_close_처리를_해준다() throws IOException { final OutputStream outputStream = mock(OutputStream.class); - /** - * todo - * try-with-resources를 사용한다. - * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. + /* + try-with-resources를 사용한다. + java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (outputStream) { + outputStream.write(1); + } catch (IOException ignored) { + } verify(outputStream, atLeastOnce()).close(); } @@ -103,20 +105,18 @@ class OutputStream_학습_테스트 { /** * InputStream 학습하기 - * - * 자바의 기본 입력 클래스는 java.io.InputStream이다. - * InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다. - * InputStream의 read() 메서드는 기반 메서드이다. + *

+ * 자바의 기본 입력 클래스는 java.io.InputStream이다. InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다. InputStream의 read() 메서드는 기반 + * 메서드이다. * public abstract int read() throws IOException; - * + *

* InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다. */ @Nested class InputStream_학습_테스트 { /** - * read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다. - * int 값을 byte 타입으로 변환하면 -128부터 127 사이의 값으로 변환된다. + * read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다. int 값을 byte 타입으로 변환하면 -128부터 127 사이의 값으로 변환된다. * 그리고 Stream 끝에 도달하면 -1을 반환한다. */ @Test @@ -124,30 +124,29 @@ class InputStream_학습_테스트 { byte[] bytes = {-16, -97, -92, -87}; final InputStream inputStream = new ByteArrayInputStream(bytes); - /** - * todo - * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? - */ - final String actual = ""; + // readAllBytes() 메서드는 InputStream의 모든 바이트를 읽어서 바이트 배열로 반환한다. + final String actual = new String(inputStream.readAllBytes()); assertThat(actual).isEqualTo("🤩"); - assertThat(inputStream.read()).isEqualTo(-1); + assertThat(inputStream.read()).isEqualTo(-1); // 스트림 끝에 도달하면 -1 반환 inputStream.close(); } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException { final InputStream inputStream = mock(InputStream.class); - /** - * todo - * try-with-resources를 사용한다. - * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. + /* + try-with-resources를 사용한다. + java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (inputStream) { + int ignored = inputStream.read(); + } catch (IOException ignored) { + } verify(inputStream, atLeastOnce()).close(); } @@ -155,26 +154,26 @@ class InputStream_학습_테스트 { /** * FilterStream 학습하기 - * - * 필터는 필터 스트림, reader, writer로 나뉜다. - * 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. - * reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다. + *

+ * 필터는 필터 스트림, reader, writer로 나뉜다. 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 + * 텍스트를 처리하는 데 사용된다. */ @Nested class FilterStream_학습_테스트 { /** - * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. - * InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. - * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? + * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. 버퍼 크기를 지정하지 + * 않으면 버퍼의 기본 사이즈는 얼마일까? */ @Test - void 필터인_BufferedInputStream를_사용해보자() { + void 필터인_BufferedInputStream를_사용해보자() throws IOException { final String text = "필터에 연결해보자."; final InputStream inputStream = new ByteArrayInputStream(text.getBytes()); - final InputStream bufferedInputStream = null; - final byte[] actual = new byte[0]; + // BufferedInputStream은 기본 버퍼 사이즈가 8192 바이트이다. + final InputStream bufferedInputStream = new BufferedInputStream(inputStream); + + final byte[] actual = bufferedInputStream.readAllBytes(); assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class); assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes()); @@ -182,31 +181,29 @@ class FilterStream_학습_테스트 { } /** - * 자바의 기본 문자열은 UTF-16 유니코드 인코딩을 사용한다. - * 문자열이 아닌 바이트 단위로 처리하려니 불편하다. - * 그리고 바이트를 문자(char)로 처리하려면 인코딩을 신경 써야 한다. - * reader, writer를 사용하면 입출력 스트림을 바이트가 아닌 문자 단위로 데이터를 처리하게 된다. - * 그리고 InputStreamReader를 사용하면 지정된 인코딩에 따라 유니코드 문자로 변환할 수 있다. + * 자바의 기본 문자열은 UTF-16 유니코드 인코딩을 사용한다. 문자열이 아닌 바이트 단위로 처리하려니 불편하다. 그리고 바이트를 문자(char)로 처리하려면 인코딩을 신경 써야 한다. reader, + * writer를 사용하면 입출력 스트림을 바이트가 아닌 문자 단위로 데이터를 처리하게 된다. 그리고 InputStreamReader를 사용하면 지정된 인코딩에 따라 유니코드 문자로 변환할 수 있다. */ @Nested class InputStreamReader_학습_테스트 { /** - * InputStreamReader를 사용해서 바이트를 문자(char)로 읽어온다. - * 읽어온 문자(char)를 문자열(String)로 처리하자. - * 필터인 BufferedReader를 사용하면 readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다. + * InputStreamReader를 사용해서 바이트를 문자(char)로 읽어온다. 읽어온 문자(char)를 문자열(String)로 처리하자. 필터인 BufferedReader를 사용하면 + * readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다. */ @Test void BufferedReader를_사용하여_문자열을_읽어온다() { - final String emoji = String.join("\r\n", + final String emoji = String.join(System.lineSeparator(), "😀😃😄😁😆😅😂🤣🥲☺️😊", "😇🙂🙃😉😌😍🥰😘😗😙😚", "😋😛😝😜🤪🤨🧐🤓😎🥸🤩", ""); final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes()); + final InputStream bufferedInputStream = new BufferedInputStream(inputStream); + final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream)); final StringBuilder actual = new StringBuilder(); - + bufferedReader.lines().forEach(str -> actual.append(str).append(System.lineSeparator())); assertThat(actual).hasToString(emoji); } } diff --git a/tomcat/src/main/java/com/techcourse/controller/ApiRouter.java b/tomcat/src/main/java/com/techcourse/controller/ApiRouter.java new file mode 100644 index 0000000000..84506c6801 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/ApiRouter.java @@ -0,0 +1,30 @@ +package com.techcourse.controller; + +import java.util.Map; +import java.util.function.Function; +import org.apache.coyote.HttpRequest; +import org.apache.coyote.HttpResponse; + +public class ApiRouter { + + private static final LoginController loginController = new LoginController(); + private static final RegisterController registerController = new RegisterController(); + + private static final Map> routingTable = Map.of( + new MethodAndPath("GET", "/login"), loginController::doGet, + new MethodAndPath("POST", "/login"), loginController::doPost, + new MethodAndPath("GET", "/register"), registerController::doGet, + new MethodAndPath("POST", "/register"), registerController::doPost + ); + + public static HttpResponse route(String method, String path, HttpRequest request) { + MethodAndPath methodAndPath = new MethodAndPath(method, path); + if (!routingTable.containsKey(methodAndPath)) { + throw new IllegalArgumentException("Handler not found for " + method + " " + path); + } + return routingTable.get(methodAndPath).apply(request); + } + + record MethodAndPath(String method, String path) { + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/LoginController.java b/tomcat/src/main/java/com/techcourse/controller/LoginController.java new file mode 100644 index 0000000000..77aa9fe6cf --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/LoginController.java @@ -0,0 +1,62 @@ +package com.techcourse.controller; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import com.techcourse.util.StaticResourceManager; +import jakarta.servlet.http.HttpSession; +import java.util.Optional; +import java.util.UUID; +import org.apache.catalina.Session; +import org.apache.catalina.SessionManager; +import org.apache.coyote.HttpRequest; +import org.apache.coyote.HttpResponse; +import org.apache.coyote.MediaType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LoginController { + + private static final Logger log = LoggerFactory.getLogger(LoginController.class); + private static final String STATIC_RESOURCE_PATH = "static/login.html"; + private static final SessionManager sessionManager = new SessionManager(); + + public HttpResponse doGet(HttpRequest request) { + MediaType mediaType = MediaType.fromAcceptHeader(request.getAccept()); + + // 로그인한 경우 index.html로 리다이렉트 + Optional session = request.getSession(sessionManager); + boolean isLogin = session.map(s -> s.getAttribute("user") != null) + .orElse(false); + if (isLogin) { + return HttpResponse.redirect("index.html"); + } + + return new HttpResponse(200, "OK") + .addHeader("Content-Type", mediaType.getValue()) + .setBody(StaticResourceManager.read(STATIC_RESOURCE_PATH)); + } + + public HttpResponse doPost(HttpRequest request) { + log.info("Query Parameters: {}", request.parseFormBody()); + + Optional userAccount = request.getFormValue("account"); + Optional userPassword = request.getFormValue("password"); + + User user = userAccount.map(InMemoryUserRepository::findByAccount) + .filter(Optional::isPresent) + .map(Optional::get) + .orElse(null); + + if (user == null || !user.checkPassword(userPassword.get())) { + return HttpResponse.redirect("401.html"); + } + + // 세션이 있으면 세션에 유저 정보를 넣어주고, 없다면 생성해서 넣어줌 + HttpSession httpSession = request.getSession(sessionManager) + .orElse(new Session(UUID.randomUUID().toString())); + httpSession.setAttribute("user", user); + sessionManager.add(httpSession); + + return HttpResponse.redirect("index.html").addCookie("JSESSIONID", httpSession.getId()); + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/RegisterController.java b/tomcat/src/main/java/com/techcourse/controller/RegisterController.java new file mode 100644 index 0000000000..dc7ccb5d91 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/RegisterController.java @@ -0,0 +1,35 @@ +package com.techcourse.controller; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import com.techcourse.util.StaticResourceManager; +import java.util.Map; +import org.apache.coyote.HttpRequest; +import org.apache.coyote.HttpResponse; +import org.apache.coyote.MediaType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RegisterController { + private static final Logger log = LoggerFactory.getLogger(RegisterController.class); + private static final String STATIC_RESOURCE_PATH = "static/register.html"; + + public HttpResponse doGet(HttpRequest request) { + MediaType mediaType = MediaType.fromAcceptHeader(request.getAccept()); + return new HttpResponse(200, "OK") + .addHeader("Content-Type", mediaType.getValue()) + .setBody(StaticResourceManager.read(STATIC_RESOURCE_PATH)); + } + + public HttpResponse doPost(HttpRequest httpRequest) { + Map body = httpRequest.parseFormBody(); + + String account = body.get("account"); + String password = body.get("password"); + String email = body.get("email"); + + User user = new User(account, password, email); + InMemoryUserRepository.save(user); + return HttpResponse.redirect("index.html"); + } +} diff --git a/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java b/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java index d3fa57feeb..42b16c5772 100644 --- a/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java +++ b/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java @@ -5,18 +5,21 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; public class InMemoryUserRepository { private static final Map database = new ConcurrentHashMap<>(); + private static final AtomicLong sequence = new AtomicLong(1); static { - final User user = new User(1L, "gugu", "password", "hkkang@woowahan.com"); + final User user = new User(sequence.getAndIncrement(), "gugu", "password", "hkkang@woowahan.com"); database.put(user.getAccount(), user); } public static void save(User user) { - database.put(user.getAccount(), user); + Long id = sequence.getAndIncrement(); + database.put(user.getAccount(), user.withId(id)); } public static Optional findByAccount(String account) { diff --git a/tomcat/src/main/java/com/techcourse/model/User.java b/tomcat/src/main/java/com/techcourse/model/User.java index e8cf4c8e68..0172bd8d0f 100644 --- a/tomcat/src/main/java/com/techcourse/model/User.java +++ b/tomcat/src/main/java/com/techcourse/model/User.java @@ -1,6 +1,12 @@ package com.techcourse.model; -public class User { +import java.io.Serial; +import java.io.Serializable; + +public class User implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; private final Long id; private final String account; @@ -18,6 +24,10 @@ public User(String account, String password, String email) { this(null, account, password, email); } + public User withId(Long id) { + return new User(id, account, password, email); + } + public boolean checkPassword(String password) { return this.password.equals(password); } diff --git a/tomcat/src/main/java/com/techcourse/util/StaticResourceManager.java b/tomcat/src/main/java/com/techcourse/util/StaticResourceManager.java new file mode 100644 index 0000000000..971297fb93 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/util/StaticResourceManager.java @@ -0,0 +1,35 @@ +package com.techcourse.util; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; + +public class StaticResourceManager { + private static final String CRLF = "\r\n"; + + private StaticResourceManager() { + } + + public static boolean isExist(String filePath) { + URL resource = ClassLoader.getSystemClassLoader().getResource(filePath); + return resource != null; + } + + public static String read(String filePath) { + URL resource = ClassLoader.getSystemClassLoader().getResource(filePath); + Path path = Optional.ofNullable(resource) + .map(URL::getPath) + .map(Path::of) + .orElseThrow(() -> new RuntimeException(filePath + " not found")); + + try { + List strings = Files.readAllLines(path); + return String.join(CRLF, strings); + } catch (IOException e) { + throw new RuntimeException(); + } + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/Session.java b/tomcat/src/main/java/org/apache/catalina/Session.java new file mode 100644 index 0000000000..9776d1ffad --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/Session.java @@ -0,0 +1,98 @@ +package org.apache.catalina; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSessionContext; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +public class Session implements HttpSession { + + private final String id; + private final Map values = new HashMap<>(); + + public Session(String id) { + this.id = id; + } + + @Override + public long getCreationTime() { + return 0; + } + + public String getId() { + return id; + } + + @Override + public long getLastAccessedTime() { + return 0; + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public void setMaxInactiveInterval(int interval) { + + } + + @Override + public int getMaxInactiveInterval() { + return 0; + } + + @Override + public HttpSessionContext getSessionContext() { + return null; + } + + public Object getAttribute(String key) { + return values.get(key); + } + + @Override + public Object getValue(String name) { + return null; + } + + @Override + public Enumeration getAttributeNames() { + return null; + } + + @Override + public String[] getValueNames() { + return new String[0]; + } + + public void setAttribute(String key, Object value) { + values.put(key, value); + } + + @Override + public void putValue(String name, Object value) { + + } + + public void removeAttribute(String key) { + values.remove(key); + } + + @Override + public void removeValue(String name) { + + } + + public void invalidate() { + values.clear(); + } + + @Override + public boolean isNew() { + return false; + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/SessionManager.java new file mode 100644 index 0000000000..35443005e0 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/SessionManager.java @@ -0,0 +1,30 @@ +package org.apache.catalina; + +import jakarta.servlet.http.HttpSession; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SessionManager implements Manager { + + private static final Map SESSIONS = new HashMap<>(); + + @Override + public void add(HttpSession session) { + SESSIONS.put(session.getId(), (Session) session); + } + + @Override + public HttpSession findSession(String id) { + return SESSIONS.get(id); + } + + @Override + public void remove(HttpSession session) { + SESSIONS.remove(session.getId()); + } + + public List getSessions() { + return List.copyOf(SESSIONS.keySet()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/HttpRequest.java new file mode 100644 index 0000000000..6703020b71 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/HttpRequest.java @@ -0,0 +1,142 @@ +package org.apache.coyote; + +import jakarta.servlet.http.HttpSession; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.apache.catalina.SessionManager; + +public class HttpRequest { + + private static final String CRLF = "\r\n"; + + private final String httpMethod; + private final String path; + private final String version; + private final Map headers; + private final Map queryParameters; + private String body; + private final Map cookies; + + public HttpRequest(String request) { + String[] lines = request.split(CRLF); + String[] requestLine = lines[0].split(" "); + String pathWithQuery = requestLine[1]; + this.httpMethod = requestLine[0]; + this.path = pathWithQuery.split("\\?")[0]; + this.version = requestLine[2]; + this.headers = parseHeaders(lines); + this.body = null; + this.queryParameters = parseQueryParameters(pathWithQuery); + this.cookies = parseCookies(headers.getOrDefault("Cookie", null)); + } + + public Optional getSession(SessionManager manager) { + String jSessionId = cookies.get("JSESSIONID"); + if (jSessionId == null) { + return Optional.empty(); + } + HttpSession session = manager.findSession(jSessionId); + return Optional.ofNullable(session); + } + + private Map parseCookies(String cookie) { + if (cookie == null) { + return new HashMap<>(); + } + + Map result = new HashMap<>(); + String[] cookies = cookie.split("; "); + for (String c : cookies) { + String[] keyValue = c.split("="); + result.put(keyValue[0], keyValue[1]); + } + return result; + } + + private Map parseHeaders(String[] lines) { + Map result = new HashMap<>(); + for (int i = 1; i < lines.length; i++) { + if (lines[i].isEmpty()) { + break; + } + String[] header = lines[i].split(": "); + result.put(header[0], header[1]); + } + return result; + } + + private Map parseQueryParameters(String path) { + Map result = new HashMap<>(); + String[] pathAndQuery = path.split("\\?"); + if (pathAndQuery.length == 2) { + String[] params = pathAndQuery[1].split("&"); + for (String param : params) { + String[] keyValue = param.split("=", -1); + result.put(keyValue[0], keyValue[1]); + } + } + return result; + } + + public String getPath() { + return path; + } + + public String getAccept() { + return headers.getOrDefault("Accept", "*/*"); + } + + public String getMethod() { + return httpMethod; + } + + public Map parseFormBody() { + Map result = new HashMap<>(); + if (body == null) { + return result; + } + String[] params = body.split("&"); + for (String param : params) { + String[] keyValue = param.split("="); + result.put(keyValue[0], keyValue[1]); + } + return result; + } + + public Optional getFormValue(String key) { + Map formBody = parseFormBody(); + if (!formBody.containsKey(key)) { + return Optional.empty(); + } + return Optional.of(formBody.get(key)); + } + + public Optional getCookie(String key) { + if (cookies == null || !cookies.containsKey(key)) { + return Optional.empty(); + } + return Optional.of(cookies.get(key)); + } + + public int getContentLength() { + return Integer.parseInt(headers.getOrDefault("Content-Length", "0")); + } + + public void setBody(String body) { + this.body = body; + } + + @Override + public String toString() { + return "HttpRequest{" + + "body='" + body + '\'' + + ", httpMethod='" + httpMethod + '\'' + + ", path='" + path + '\'' + + ", version='" + version + '\'' + + ", headers=" + headers + + ", queryParameters=" + queryParameters + + ", cookies=" + cookies + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/HttpResponse.java new file mode 100644 index 0000000000..d0c4fe59ec --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/HttpResponse.java @@ -0,0 +1,59 @@ +package org.apache.coyote; + +import java.util.HashMap; +import java.util.Map; + +public class HttpResponse { + private static final double VERSION = 1.1; + + private final int statusCode; + private final String statusMessage; + private final Map headers; + private String body; + + public HttpResponse(int statusCode, String statusMessage) { + this.statusCode = statusCode; + this.statusMessage = statusMessage; + this.headers = new HashMap<>(); + this.body = ""; + } + + public static HttpResponse redirect(String location) { + return new HttpResponse(302, "Found") + .addHeader("Location", location) + .setBody(""); + } + + public HttpResponse addHeader(String key, String value) { + headers.put(key, value); + return this; + } + + public HttpResponse setBody(String body) { + this.body = body; + headers.put("Content-Length", String.valueOf(body.getBytes().length)); + return this; + } + + public HttpResponse addCookie(String key, String value) { + if (headers.containsKey("Set-Cookie")) { + headers.put("Set-Cookie", headers.get("Set-Cookie") + "; " + key + "=" + value); + return this; + } + headers.put("Set-Cookie", key + "=" + value); + return this; + } + + public String toHttpMessage() { + final var response = new StringBuilder(); + response.append("HTTP/").append(VERSION).append(" ").append(statusCode).append(" ").append(statusMessage).append("\r\n"); + headers.forEach((key, value) -> response.append(key).append(": ").append(value).append("\r\n")); + response.append("\r\n"); + response.append(body); + return response.toString(); + } + + public byte[] getAsBytes() { + return toHttpMessage().getBytes(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/MediaType.java b/tomcat/src/main/java/org/apache/coyote/MediaType.java new file mode 100644 index 0000000000..52d564f15e --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/MediaType.java @@ -0,0 +1,99 @@ +package org.apache.coyote; + +import java.util.Arrays; +import java.util.Set; + +public enum MediaType { + + APPLICATION_JSON(Type.APPLICATION, SubType.JSON, Set.of("json")), + TEXT_HTML(Type.TEXT, SubType.HTML, Set.of("html")), + TEXT_CSS(Type.TEXT, SubType.CSS, Set.of("css")), + TEXT_PLAIN(Type.TEXT, SubType.PLAIN, Set.of("txt")), + ALL(Type.ALL, SubType.ALL, Set.of()); + + private final Type type; + private final SubType subType; + private final Set extensions; + + MediaType(Type type, SubType subType, Set extensions) { + this.type = type; + this.subType = subType; + this.extensions = extensions; + } + + public String getValue() { + return type.getValue() + "/" + subType.getValue(); + } + + public static MediaType fromAcceptHeader(String header) { + if (header == null) { + return ALL; + } + + String[] values = header.split(","); + for (String value : values) { + String[] mediaType = value.split(";"); + if (mediaType.length == 1) { + return of(mediaType[0]); + } + if (mediaType.length == 2 && mediaType[1].trim().equals("q=0")) { + continue; + } + return of(mediaType[0]); + } + return ALL; + } + + public static MediaType of(String value) { + for (MediaType mediaType : values()) { + if (mediaType.getValue().equals(value)) { + return mediaType; + } + } + return ALL; + } + + public static MediaType fromExtension(String extension) { + return Arrays.stream(values()) + .filter(mediaType -> mediaType.extensions.contains(extension)) + .findFirst() + .orElse(ALL); + } + + public enum Type { + TEXT("text"), + APPLICATION("application"), + IMAGE("image"), + AUDIO("audio"), + VIDEO("video"), + ALL("*"); + + private final String value; + + Type(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + public enum SubType { + JSON("json"), + HTML("html"), + CSS("css"), + PLAIN("plain"), + ALL("*"); + + private final String value; + + SubType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } +} 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..61a554f111 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,16 +1,26 @@ package org.apache.coyote.http11; +import com.techcourse.controller.ApiRouter; import com.techcourse.exception.UncheckedServletException; +import com.techcourse.util.StaticResourceManager; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import org.apache.catalina.SessionManager; +import org.apache.coyote.HttpRequest; +import org.apache.coyote.HttpResponse; +import org.apache.coyote.MediaType; import org.apache.coyote.Processor; 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 SessionManager sessionManager = new SessionManager(); private final Socket connection; @@ -29,19 +39,61 @@ public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); final var outputStream = connection.getOutputStream()) { - final var responseBody = "Hello world!"; - - 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); + HttpRequest request = readRequest(inputStream); + HttpResponse response = createResponse(request); - outputStream.write(response.getBytes()); - outputStream.flush(); + writeResponse(outputStream, response); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } + log.info("현재 세션: {}", sessionManager.getSessions()); + } + + private HttpRequest readRequest(InputStream inputStream) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + StringBuilder sb = new StringBuilder(); + String line; + + while ((line = reader.readLine()) != null) { + sb.append(line).append("\r\n"); + if (line.isEmpty()) { + break; + } + } + HttpRequest request = new HttpRequest(sb.toString()); + + if (request.getContentLength() > 0) { + char[] body = new char[request.getContentLength()]; + reader.read(body); + request.setBody(new String(body)); + } + log.info("Request: {}", request); + + return request; + } + + private HttpResponse createResponse(HttpRequest request) { + if (StaticResourceManager.isExist("static" + request.getPath())) { + return createStaticResourceResponse(request); + } + return ApiRouter.route(request.getMethod(), request.getPath(), request); + } + + private HttpResponse createStaticResourceResponse(HttpRequest request) { + String path = request.getPath(); + int extensionSeparatorIndex = path.lastIndexOf("."); + String requestedExtension = extensionSeparatorIndex == -1 ? "" : path.substring(extensionSeparatorIndex + 1); + MediaType mediaType = MediaType.fromExtension(requestedExtension); + log.info("Requested MediaType: {}", mediaType); + + String responseBody = StaticResourceManager.read("static" + request.getPath()); + return new HttpResponse(200, "OK") + .addHeader("Content-Type", mediaType.getValue()) + .setBody(responseBody); + } + + private void writeResponse(OutputStream outputStream, HttpResponse response) throws IOException { + outputStream.write(response.getAsBytes()); + outputStream.flush(); } } 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/support/StubSocket.java b/tomcat/src/test/java/support/StubSocket.java index 6ba8ba5ef9..1b06d389bf 100644 --- a/tomcat/src/test/java/support/StubSocket.java +++ b/tomcat/src/test/java/support/StubSocket.java @@ -23,6 +23,7 @@ public StubSocket() { this("GET / HTTP/1.1\r\nHost: localhost:8080\r\n\r\n"); } + @Override public InetAddress getInetAddress() { try { return InetAddress.getLocalHost(); @@ -31,14 +32,17 @@ public InetAddress getInetAddress() { } } + @Override public int getPort() { return 8080; } + @Override public InputStream getInputStream() { return new ByteArrayInputStream(request.getBytes()); } + @Override public OutputStream getOutputStream() { return new OutputStream() { @Override