diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java index 182421e83c0..cb243896778 100644 --- a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java @@ -22,6 +22,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * #L% */ import java.io.*; +import java.util.List; import java.util.Map.Entry; import javax.ws.rs.core.MediaType; @@ -104,7 +105,7 @@ public class JaxRsResponse extends RestfulResponse { private ResponseBuilder buildResponse(int statusCode) { ResponseBuilder response = Response.status(statusCode); - for (Entry header : getHeaders().entrySet()) { + for (Entry> header : getHeaders().entrySet()) { response.header(header.getKey(), header.getValue()); } return response; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java index 2bd94a0047e..d56026d2046 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java @@ -21,9 +21,7 @@ package ca.uhn.fhir.rest.server; */ import java.io.IOException; -import java.util.Date; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; +import java.util.*; import org.hl7.fhir.instance.model.api.*; @@ -35,7 +33,7 @@ public abstract class RestfulResponse implements IRest private IIdType myOperationResourceId; private IPrimitiveType myOperationResourceLastUpdated; - private ConcurrentHashMap theHeaders = new ConcurrentHashMap(); + private Map> theHeaders = new HashMap<>(); private T theRequestDetails; public RestfulResponse(T requestDetails) { @@ -44,14 +42,14 @@ public abstract class RestfulResponse implements IRest @Override public void addHeader(String headerKey, String headerValue) { - this.getHeaders().put(headerKey, headerValue); + this.getHeaders().computeIfAbsent(headerKey, k -> new ArrayList<>()).add(headerValue); } /** * Get the http headers * @return the headers */ - public ConcurrentHashMap getHeaders() { + public Map> getHeaders() { return theHeaders; } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponse.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponse.java index 3ffb3d9b9f1..969c0fa12b6 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponse.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponse.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; +import java.util.List; import java.util.Map.Entry; import java.util.zip.GZIPOutputStream; @@ -75,8 +76,18 @@ public class ServletRestfulResponse extends RestfulResponse header : getHeaders().entrySet()) { - theHttpResponse.setHeader(header.getKey(), header.getValue()); + for (Entry> header : getHeaders().entrySet()) { + final String key = header.getKey(); + boolean first = true; + for (String value : header.getValue()) { + // existing headers should be overridden + if (first) { + theHttpResponse.setHeader(key, value); + first = false; + } else { + theHttpResponse.addHeader(key, value); + } + } } } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulResponseTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulResponseTest.java new file mode 100644 index 00000000000..009dbe3af5b --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulResponseTest.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.rest.server; + +import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.MockSettings; +import org.mockito.Mockito; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +/** + * Unit tests of {@link RestfulResponse}. + */ +public class RestfulResponseTest { + @Test + public void addMultipleHeaderValues() { + @SuppressWarnings("unchecked") + final RestfulResponse restfulResponse = + mock(RestfulResponse.class, withSettings() + .useConstructor((RequestDetails) null).defaultAnswer(CALLS_REAL_METHODS)); + + restfulResponse.addHeader("Authorization", "Basic"); + restfulResponse.addHeader("Authorization", "Bearer"); + restfulResponse.addHeader("Cache-Control", "no-cache, no-store"); + + assertEquals(2, restfulResponse.getHeaders().size()); + assertThat(restfulResponse.getHeaders().get("Authorization"), Matchers.contains("Basic", "Bearer")); + assertThat(restfulResponse.getHeaders().get("Cache-Control"), Matchers.contains("no-cache, no-store")); + } +} diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponseTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponseTest.java new file mode 100644 index 00000000000..f56e475f7a2 --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponseTest.java @@ -0,0 +1,65 @@ +package ca.uhn.fhir.rest.server.servlet; + +import ca.uhn.fhir.rest.server.RestfulServer; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * Unit tests of {@link ServletRestfulResponse}. + */ +public class ServletRestfulResponseTest { + @Mock + private RestfulServer server; + + @Mock + private ServletOutputStream servletOutputStream; + + @Mock + private HttpServletResponse servletResponse; + + private ServletRequestDetails requestDetails; + + private ServletRestfulResponse response; + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Before + public void init() throws IOException { + Mockito.when(servletResponse.getOutputStream()).thenReturn(servletOutputStream); + + requestDetails = new ServletRequestDetails(); + requestDetails.setServer(server); + requestDetails.setServletResponse(servletResponse); + response = new ServletRestfulResponse(requestDetails); + } + + @Test + public void addMultipleHeaderValues() throws IOException { + final ServletRestfulResponse response = new ServletRestfulResponse(requestDetails); + response.addHeader("Authorization", "Basic"); + response.addHeader("Authorization", "Bearer"); + response.addHeader("Cache-Control", "no-cache, no-store"); + + response.getResponseWriter(200, "Status", "text/plain", "UTF-8", false); + + final InOrder orderVerifier = Mockito.inOrder(servletResponse); + orderVerifier.verify(servletResponse).setHeader(eq("Authorization"), eq("Basic")); + orderVerifier.verify(servletResponse).addHeader(eq("Authorization"), eq("Bearer")); + verify(servletResponse).setHeader(eq("Cache-Control"), eq("no-cache, no-store")); + } +}