From 22e48de815e3066c529ce943e2b334b5703250ca Mon Sep 17 00:00:00 2001 From: Kinshuk Bairagi Date: Fri, 9 Aug 2024 11:55:09 +0530 Subject: [PATCH 1/6] Stage1 --- .../filter/CustomHeaderHttpFilter.java | 41 +++++++++++ .../helloworld/guice/HelloWorldModule.java | 7 +- guice/build.gradle | 1 + .../FilterServletResponseWrapper.java | 69 +++++++++++++++++++ .../interceptor/HttpFilterInterceptor.java | 67 ++++++++++-------- 5 files changed, 156 insertions(+), 29 deletions(-) create mode 100644 examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java create mode 100644 guice/src/main/java/com/flipkart/gjex/http/interceptor/FilterServletResponseWrapper.java diff --git a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java new file mode 100644 index 00000000..828eb946 --- /dev/null +++ b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java @@ -0,0 +1,41 @@ +package com.flipkart.gjex.examples.helloworld.filter; + +import com.flipkart.gjex.core.filter.RequestParams; +import com.flipkart.gjex.core.filter.http.HttpFilter; + +import javax.inject.Named; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +@Named("CustomHeaderHttpFilter") +public class CustomHeaderHttpFilter extends HttpFilter { + + @Override + public void doProcessRequest(ServletRequest servletRequest, RequestParams> requestParams) { + super.doProcessRequest(servletRequest, requestParams); + } + + @Override + public void doProcessResponseHeaders(Map responseHeaders) { + super.doProcessResponseHeaders(responseHeaders); + } + + @Override + public void doProcessResponse(ServletResponse response) { + super.doProcessResponse(response); + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + httpServletResponse.addHeader("x-custom-header", "custom-header-value"); + } + + @Override + public void doHandleException(Exception e) { + super.doHandleException(e); + } + + @Override + public HttpFilter getInstance() { + return new CustomHeaderHttpFilter(); + } +} diff --git a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/guice/HelloWorldModule.java b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/guice/HelloWorldModule.java index 060c188d..c8ca9f2f 100644 --- a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/guice/HelloWorldModule.java +++ b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/guice/HelloWorldModule.java @@ -16,9 +16,11 @@ package com.flipkart.gjex.examples.helloworld.guice; import com.flipkart.gjex.core.filter.grpc.GrpcFilter; +import com.flipkart.gjex.core.filter.http.HttpFilterParams; import com.flipkart.gjex.core.filter.http.JavaxFilterParams; import com.flipkart.gjex.core.tracing.TracingSampler; import com.flipkart.gjex.examples.helloworld.filter.AuthFilter; +import com.flipkart.gjex.examples.helloworld.filter.CustomHeaderHttpFilter; import com.flipkart.gjex.examples.helloworld.filter.LoggingFilter; import com.flipkart.gjex.examples.helloworld.service.GreeterService; import com.flipkart.gjex.examples.helloworld.tracing.AllWhitelistTracingSampler; @@ -54,7 +56,8 @@ protected void configure() { // bind(AccessLogGrpcFilter.class).to(AccessLogTestFilter.class); bind(TracingSampler.class).to(AllWhitelistTracingSampler.class); bind(ResourceConfig.class).annotatedWith(Names.named("HelloWorldResourceConfig")).to(HelloWorldResourceConfig.class); - bind(JavaxFilterParams.class).annotatedWith(Names.named("ExampleJavaxFilter")) - .toInstance(JavaxFilterParams.builder().filter(new ExampleJavaxFilter()).pathSpec("/*").build()); + bind(JavaxFilterParams.class).annotatedWith(Names.named("ExampleJavaxFilter")).toInstance(JavaxFilterParams.builder().filter(new ExampleJavaxFilter()).pathSpec("/*").build()); + bind(HttpFilterParams.class).annotatedWith(Names.named("CustomHeaderHttpFilter")).toInstance(HttpFilterParams.builder().filter(new CustomHeaderHttpFilter()).pathSpec("/*").build()); + } } diff --git a/guice/build.gradle b/guice/build.gradle index 9c4657c9..35db57de 100644 --- a/guice/build.gradle +++ b/guice/build.gradle @@ -66,6 +66,7 @@ dependencies { implementation 'org.eclipse.jetty:jetty-webapp:9.4.22.v20191022' implementation 'io.opentracing.brave:brave-opentracing:0.31.3' implementation 'io.zipkin.reporter2:zipkin-sender-okhttp3:2.7.7' + implementation 'commons-io:commons-io:2.16.1' implementation 'io.prometheus:prometheus-metrics-exporter-servlet-javax:1.2.0' diff --git a/guice/src/main/java/com/flipkart/gjex/http/interceptor/FilterServletResponseWrapper.java b/guice/src/main/java/com/flipkart/gjex/http/interceptor/FilterServletResponseWrapper.java new file mode 100644 index 00000000..c64f9c96 --- /dev/null +++ b/guice/src/main/java/com/flipkart/gjex/http/interceptor/FilterServletResponseWrapper.java @@ -0,0 +1,69 @@ +package com.flipkart.gjex.http.interceptor; + +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintWriter; + +public class FilterServletResponseWrapper extends HttpServletResponseWrapper { + + ServletOutputStreamWrapper stream = new ServletOutputStreamWrapper(); + + /** + * Constructs a request object wrapping the given request. + * + * @param request + * @throws IllegalArgumentException if the request is null + */ + public FilterServletResponseWrapper(HttpServletResponse request) { + super(request); + } + + public ServletOutputStream getOutputStream() throws IOException + { + return stream; + } + + public PrintWriter getWriter() throws IOException + { + return new PrintWriter(stream); + } + + public byte[] getWrapperBytes() + { + return stream.getBytes(); + } + + + static class ServletOutputStreamWrapper extends ServletOutputStream { + + private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + public void write(int b) throws IOException { + out.write(b); + } + + public byte[] getBytes() { + return out.toByteArray(); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + try { + writeListener.onWritePossible(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + } + +} diff --git a/guice/src/main/java/com/flipkart/gjex/http/interceptor/HttpFilterInterceptor.java b/guice/src/main/java/com/flipkart/gjex/http/interceptor/HttpFilterInterceptor.java index 0de4f000..64aa7843 100644 --- a/guice/src/main/java/com/flipkart/gjex/http/interceptor/HttpFilterInterceptor.java +++ b/guice/src/main/java/com/flipkart/gjex/http/interceptor/HttpFilterInterceptor.java @@ -4,24 +4,27 @@ import com.flipkart.gjex.core.filter.http.HttpFilter; import com.flipkart.gjex.core.filter.http.HttpFilterParams; import org.eclipse.jetty.http.pathmap.ServletPathSpec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.inject.Named; import javax.inject.Singleton; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; +import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Singleton @Named("HttpFilterInterceptor") public class HttpFilterInterceptor implements javax.servlet.Filter { + private static final Logger logger = LoggerFactory.getLogger(HttpFilterInterceptor.class); + private static class ServletPathFiltersHolder { ServletPathSpec spec; HttpFilter filter; @@ -62,32 +65,42 @@ public void init(FilterConfig filterConfig) throws ServletException {} public final void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - List filters = new ArrayList<>(); RequestParams.RequestParamsBuilder> requestParamsBuilder = RequestParams.builder(); - try { - if (request instanceof HttpServletRequest){ - HttpServletRequest httpServletRequest = (HttpServletRequest) request; - filters = getMatchingFilters(httpServletRequest.getRequestURI()); - - Map headers = Collections.list(httpServletRequest.getHeaderNames()) - .stream().collect(Collectors.toMap(h -> h, httpServletRequest::getHeader)); - requestParamsBuilder.metadata(headers); - requestParamsBuilder.clientIp(getClientIp(request)); - requestParamsBuilder.method(httpServletRequest.getMethod()); - requestParamsBuilder.resourcePath(getFullURL(httpServletRequest)); - } + + if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) { + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + + List filters = getMatchingFilters(httpServletRequest.getRequestURI()); + Map headers = Collections.list(httpServletRequest.getHeaderNames()) + .stream().collect(Collectors.toMap(h -> h, httpServletRequest::getHeader)); + requestParamsBuilder.metadata(headers); + requestParamsBuilder.clientIp(getClientIp(request)); + requestParamsBuilder.method(httpServletRequest.getMethod()); + requestParamsBuilder.resourcePath(getFullURL(httpServletRequest)); + RequestParams> requestParams = requestParamsBuilder.build(); - filters.forEach(filter -> filter.doProcessRequest(request, requestParams)); - chain.doFilter(request, response); - } finally { - if (response instanceof HttpServletResponse) { - HttpServletResponse httpServletResponse = (HttpServletResponse) response; - Map headers = httpServletResponse.getHeaderNames() + FilterServletResponseWrapper responseWrapper = new FilterServletResponseWrapper(httpServletResponse); + + try { + filters.forEach(filter -> filter.doProcessRequest(request, requestParams)); + chain.doFilter(request, responseWrapper); + Map responseHeaders = responseWrapper.getHeaderNames() .stream().collect(Collectors.toMap(h -> h, httpServletResponse::getHeader)); - filters.forEach(filter -> filter.doProcessResponseHeaders(headers)); + filters.forEach(filter -> filter.doProcessResponseHeaders(responseHeaders)); + + } finally { + filters.forEach(filter -> filter.doProcessResponse(responseWrapper)); + response.getOutputStream().write(responseWrapper.getWrapperBytes()); } - filters.forEach(filter -> filter.doProcessResponse(response)); + + } else { + // For Unsupported request types, pass the request and response as is + chain.doFilter(request, response); + logger.warn("Unsupported request type {}, pass the request and response as is.", request.getClass()); } + + } /** From fc7843ba86a6c20e0982bbd13d6c589f5565ef64 Mon Sep 17 00:00:00 2001 From: Kinshuk Bairagi Date: Fri, 9 Aug 2024 12:00:13 +0530 Subject: [PATCH 2/6] Allow modifications from http-filters --- .../examples/helloworld/filter/CustomHeaderHttpFilter.java | 3 ++- .../gjex/http/interceptor/HttpFilterInterceptor.java | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java index 828eb946..35f54596 100644 --- a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java +++ b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java @@ -20,13 +20,14 @@ public void doProcessRequest(ServletRequest servletRequest, RequestParams responseHeaders) { super.doProcessResponseHeaders(responseHeaders); + responseHeaders.put("x-custom-doProcessResponseHeaders", "custom-header-value"); } @Override public void doProcessResponse(ServletResponse response) { super.doProcessResponse(response); HttpServletResponse httpServletResponse = (HttpServletResponse) response; - httpServletResponse.addHeader("x-custom-header", "custom-header-value"); + httpServletResponse.addHeader("x-custom-doProcessResponse", "custom-header-value"); } @Override diff --git a/guice/src/main/java/com/flipkart/gjex/http/interceptor/HttpFilterInterceptor.java b/guice/src/main/java/com/flipkart/gjex/http/interceptor/HttpFilterInterceptor.java index 64aa7843..ff3bc6d0 100644 --- a/guice/src/main/java/com/flipkart/gjex/http/interceptor/HttpFilterInterceptor.java +++ b/guice/src/main/java/com/flipkart/gjex/http/interceptor/HttpFilterInterceptor.java @@ -85,11 +85,14 @@ public final void doFilter(ServletRequest request, ServletResponse response, try { filters.forEach(filter -> filter.doProcessRequest(request, requestParams)); chain.doFilter(request, responseWrapper); + + // Allow the filters to process the response headers Map responseHeaders = responseWrapper.getHeaderNames() .stream().collect(Collectors.toMap(h -> h, httpServletResponse::getHeader)); filters.forEach(filter -> filter.doProcessResponseHeaders(responseHeaders)); - + responseHeaders.forEach(responseWrapper::setHeader); } finally { + // Allow the filters to process the response filters.forEach(filter -> filter.doProcessResponse(responseWrapper)); response.getOutputStream().write(responseWrapper.getWrapperBytes()); } From f60487493997fec01801f512c4a9317644a9313a Mon Sep 17 00:00:00 2001 From: Kinshuk Bairagi Date: Fri, 9 Aug 2024 16:38:44 +0530 Subject: [PATCH 3/6] Update FilterServletResponseWrapper.java --- .../FilterServletResponseWrapper.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/guice/src/main/java/com/flipkart/gjex/http/interceptor/FilterServletResponseWrapper.java b/guice/src/main/java/com/flipkart/gjex/http/interceptor/FilterServletResponseWrapper.java index c64f9c96..8f6b6fc6 100644 --- a/guice/src/main/java/com/flipkart/gjex/http/interceptor/FilterServletResponseWrapper.java +++ b/guice/src/main/java/com/flipkart/gjex/http/interceptor/FilterServletResponseWrapper.java @@ -10,7 +10,8 @@ public class FilterServletResponseWrapper extends HttpServletResponseWrapper { - ServletOutputStreamWrapper stream = new ServletOutputStreamWrapper(); + private final ServletOutputStreamWrapper stream = new ServletOutputStreamWrapper(); + private PrintWriter pw; /** * Constructs a request object wrapping the given request. @@ -22,18 +23,19 @@ public FilterServletResponseWrapper(HttpServletResponse request) { super(request); } - public ServletOutputStream getOutputStream() throws IOException - { + public ServletOutputStream getOutputStream() throws IOException { + if (pw != null) { + pw.flush(); + } return stream; } - public PrintWriter getWriter() throws IOException - { - return new PrintWriter(stream); + public PrintWriter getWriter() throws IOException { + pw = new PrintWriter(stream); + return pw; } - public byte[] getWrapperBytes() - { + public byte[] getWrapperBytes() { return stream.getBytes(); } From ce1411ba31cbcf88ebb1e443d2c46549a5a05c00 Mon Sep 17 00:00:00 2001 From: Kinshuk Bairagi Date: Sat, 10 Aug 2024 17:06:08 +0530 Subject: [PATCH 4/6] Update CustomHeaderHttpFilter.java --- .../examples/helloworld/filter/CustomHeaderHttpFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java index 35f54596..c884475f 100644 --- a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java +++ b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java @@ -20,14 +20,14 @@ public void doProcessRequest(ServletRequest servletRequest, RequestParams responseHeaders) { super.doProcessResponseHeaders(responseHeaders); - responseHeaders.put("x-custom-doProcessResponseHeaders", "custom-header-value"); + responseHeaders.put("x-custom-header1", "value1"); } @Override public void doProcessResponse(ServletResponse response) { super.doProcessResponse(response); HttpServletResponse httpServletResponse = (HttpServletResponse) response; - httpServletResponse.addHeader("x-custom-doProcessResponse", "custom-header-value"); + httpServletResponse.addHeader("x-custom-header2", "value2"); } @Override From b4ccbcfe45549961da272e53d5dc4ba8f8cae25c Mon Sep 17 00:00:00 2001 From: Kinshuk Bairagi Date: Sat, 10 Aug 2024 17:14:13 +0530 Subject: [PATCH 5/6] Update build.gradle --- guice/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/guice/build.gradle b/guice/build.gradle index 35db57de..9c4657c9 100644 --- a/guice/build.gradle +++ b/guice/build.gradle @@ -66,7 +66,6 @@ dependencies { implementation 'org.eclipse.jetty:jetty-webapp:9.4.22.v20191022' implementation 'io.opentracing.brave:brave-opentracing:0.31.3' implementation 'io.zipkin.reporter2:zipkin-sender-okhttp3:2.7.7' - implementation 'commons-io:commons-io:2.16.1' implementation 'io.prometheus:prometheus-metrics-exporter-servlet-javax:1.2.0' From 3c411e82717cbab6992cde7cb2ea8215b4d55a07 Mon Sep 17 00:00:00 2001 From: Kinshuk Bairagi Date: Sat, 10 Aug 2024 17:16:42 +0530 Subject: [PATCH 6/6] licence header --- .../filter/CustomHeaderHttpFilter.java | 15 ++++ .../web/javaxfilter/ExampleJavaxFilter.java | 15 ++++ .../FilterServletResponseWrapper.java | 70 +++++++++++++++++-- 3 files changed, 94 insertions(+), 6 deletions(-) diff --git a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java index c884475f..007baef7 100644 --- a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java +++ b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/CustomHeaderHttpFilter.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) The original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.flipkart.gjex.examples.helloworld.filter; import com.flipkart.gjex.core.filter.RequestParams; diff --git a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/web/javaxfilter/ExampleJavaxFilter.java b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/web/javaxfilter/ExampleJavaxFilter.java index 37ba4044..9e0c2171 100644 --- a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/web/javaxfilter/ExampleJavaxFilter.java +++ b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/web/javaxfilter/ExampleJavaxFilter.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) The original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.flipkart.gjex.examples.helloworld.web.javaxfilter; import com.flipkart.gjex.core.logging.Logging; diff --git a/guice/src/main/java/com/flipkart/gjex/http/interceptor/FilterServletResponseWrapper.java b/guice/src/main/java/com/flipkart/gjex/http/interceptor/FilterServletResponseWrapper.java index 8f6b6fc6..f545d07d 100644 --- a/guice/src/main/java/com/flipkart/gjex/http/interceptor/FilterServletResponseWrapper.java +++ b/guice/src/main/java/com/flipkart/gjex/http/interceptor/FilterServletResponseWrapper.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) The original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.flipkart.gjex.http.interceptor; import javax.servlet.ServletOutputStream; @@ -8,21 +23,30 @@ import java.io.IOException; import java.io.PrintWriter; +/** + * A wrapper for HttpServletResponse that captures the output stream and writer. + */ public class FilterServletResponseWrapper extends HttpServletResponseWrapper { private final ServletOutputStreamWrapper stream = new ServletOutputStreamWrapper(); private PrintWriter pw; /** - * Constructs a request object wrapping the given request. + * Constructs a response object wrapping the given response. * - * @param request - * @throws IllegalArgumentException if the request is null + * @param response the HttpServletResponse to be wrapped + * @throws IllegalArgumentException if the response is null */ - public FilterServletResponseWrapper(HttpServletResponse request) { - super(request); + public FilterServletResponseWrapper(HttpServletResponse response) { + super(response); } + /** + * Returns the ServletOutputStream for this response. + * + * @return the ServletOutputStream + * @throws IOException if an I/O error occurs + */ public ServletOutputStream getOutputStream() throws IOException { if (pw != null) { pw.flush(); @@ -30,33 +54,67 @@ public ServletOutputStream getOutputStream() throws IOException { return stream; } + /** + * Returns a PrintWriter for this response. + * + * @return the PrintWriter + * @throws IOException if an I/O error occurs + */ public PrintWriter getWriter() throws IOException { pw = new PrintWriter(stream); return pw; } + /** + * Returns the captured bytes from the output stream. + * + * @return a byte array containing the captured bytes + */ public byte[] getWrapperBytes() { return stream.getBytes(); } - + /** + * A wrapper for ServletOutputStream that captures written bytes. + */ static class ServletOutputStreamWrapper extends ServletOutputStream { private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + /** + * Writes the specified byte to this output stream. + * + * @param b the byte to be written + * @throws IOException if an I/O error occurs + */ public void write(int b) throws IOException { out.write(b); } + /** + * Returns the captured bytes from the output stream. + * + * @return a byte array containing the captured bytes + */ public byte[] getBytes() { return out.toByteArray(); } + /** + * Indicates whether this output stream is ready to be written to. + * + * @return true if the output stream is ready, false otherwise + */ @Override public boolean isReady() { return true; } + /** + * Sets the WriteListener for this output stream. + * + * @param writeListener the WriteListener to be set + */ @Override public void setWriteListener(WriteListener writeListener) { try {