SimilarInputStreamTest.java
/*
* Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.jersey.tests.e2e.server;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.internal.InternalProperties;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.jetty.JettyHttpContainerFactory;
import org.glassfish.jersey.message.internal.ReaderWriter;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.glassfish.jersey.test.JerseyTest;
import org.glassfish.jersey.test.spi.TestContainer;
import org.glassfish.jersey.test.spi.TestContainerException;
import org.glassfish.jersey.test.spi.TestContainerFactory;
import org.junit.jupiter.api.Test;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.Collections;
import java.util.EnumSet;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class SimilarInputStreamTest extends JerseyTest {
@Override
protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
return (baseUri, deploymentContext) -> {
final Server server = JettyHttpContainerFactory.createServer(baseUri, false);
final ServerConnector connector = new ServerConnector(server);
connector.setPort(9001);
server.addConnector(connector);
final ServletContainer jerseyServletContainer = new ServletContainer(deploymentContext.getResourceConfig());
final ServletHolder jettyServletHolder = new ServletHolder(jerseyServletContainer);
final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
context.setContextPath("/");
// filter which will change the http servlet request to have a reply-able input stream
context.addFilter(FilterSettingMultiReadRequest.class,
"/*", EnumSet.allOf(DispatcherType.class));
context.addServlet(jettyServletHolder, "/api/*");
server.setHandler(context);
return new TestContainer() {
@Override
public ClientConfig getClientConfig() {
return new ClientConfig();
}
@Override
public URI getBaseUri() {
return baseUri;
}
@Override
public void start() {
try {
server.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void stop() {
try {
server.stop();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
};
};
}
@Override
protected Application configure() {
ResourceConfig resourceConfig = new ResourceConfig(TestResource.class);
// force jersey to use jackson for deserialization
resourceConfig.addProperties(
Collections.singletonMap(InternalProperties.JSON_FEATURE, JacksonFeature.class.getSimpleName()));
return resourceConfig;
}
@Test
public void readJsonWithReplayableInputStreamFailsTest() {
final Invocation.Builder requestBuilder = target("/api/v1/echo").request();
final MyDto myDto = new MyDto();
myDto.setMyField("Something");
try (Response response = requestBuilder.post(Entity.entity(myDto, MediaType.APPLICATION_JSON))) {
// fixed from failure with a 400 as jackson can never finish reading the input stream
assertEquals(200, response.getStatus());
final MyDto resultDto = response.readEntity(MyDto.class);
assertEquals("Something", resultDto.getMyField()); //verify we still get Something
}
}
@Path("/v1")
public static class TestResource {
@POST
@Path("/echo")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public MyDto echo(MyDto input) {
return input;
}
}
public static class MyDto {
private String myField;
public String getMyField() {
return myField;
}
public void setMyField(String myField) {
this.myField = myField;
}
@Override
public String toString() {
return "MyDto{"
+ "myField='" + myField + '\''
+ '}';
}
}
public static class FilterSettingMultiReadRequest implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
/* wrap the request in order to read the inputstream multiple times */
MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request);
chain.doFilter(multiReadRequest, response);
}
}
static class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
private byte[] cachedBytes;
public MultiReadHttpServletRequest(HttpServletRequest request) {
super(request);
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (cachedBytes == null) {
cacheInputStream();
}
return new CachedServletInputStream(cachedBytes);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
private void cacheInputStream() throws IOException {
// Cache the inputstream in order to read it multiple times.
cachedBytes = ReaderWriter.readFromAsBytes(super.getInputStream());
}
/* An input stream which reads the cached request body */
private class CachedServletInputStream extends ServletInputStream {
private final ByteArrayInputStream buffer;
public CachedServletInputStream(byte[] contents) {
this.buffer = new ByteArrayInputStream(contents);
}
@Override
public int read() {
return buffer.read();
}
@Override
public boolean isFinished() {
return buffer.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new RuntimeException("Not implemented");
}
}
}
}