diff --git a/src/main/java/edu/wisc/my/restproxy/dao/RestProxyDao.java b/src/main/java/edu/wisc/my/restproxy/dao/RestProxyDao.java index 81fb35d..7d56ff2 100644 --- a/src/main/java/edu/wisc/my/restproxy/dao/RestProxyDao.java +++ b/src/main/java/edu/wisc/my/restproxy/dao/RestProxyDao.java @@ -3,6 +3,8 @@ */ package edu.wisc.my.restproxy.dao; +import org.springframework.http.ResponseEntity; + import edu.wisc.my.restproxy.ProxyRequestContext; /** @@ -13,9 +15,8 @@ public interface RestProxyDao { /** - * * @param proxyRequestContext - * @return the response of the proxied request, serialized to an {@link Object} + * @return the {@link ResponseEntity} of the proxied request. */ - public Object proxyRequest(ProxyRequestContext proxyRequestContext); + public ResponseEntity proxyRequest(ProxyRequestContext proxyRequestContext); } diff --git a/src/main/java/edu/wisc/my/restproxy/dao/RestProxyDaoImpl.java b/src/main/java/edu/wisc/my/restproxy/dao/RestProxyDaoImpl.java index a676e68..5c87575 100644 --- a/src/main/java/edu/wisc/my/restproxy/dao/RestProxyDaoImpl.java +++ b/src/main/java/edu/wisc/my/restproxy/dao/RestProxyDaoImpl.java @@ -9,6 +9,7 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -21,23 +22,33 @@ /** * {@link RestProxyDao} implementation backed by a {@link RestTemplate}. * - * A default instance is provided, but consumers are strongly recommended to configure - * their own instance and inject. + * A default {@link RestTemplate} instance is provided, but consumers are strongly recommended to + * configure their own instance and inject. However, this class will always use + * {@link RestProxyResponseErrorHandler} because it's the client's responsiblity to deal with + * errors. * * @author Nicholas Blair */ @Service -public class RestProxyDaoImpl implements RestProxyDao { - +public class RestProxyDaoImpl implements RestProxyDao, InitializingBean { + @Autowired(required=false) private RestTemplate restTemplate = new RestTemplate(); + /* (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() throws Exception { + this.restTemplate.setErrorHandler(new RestProxyResponseErrorHandler()); + }; + private static final Logger logger = LoggerFactory.getLogger(RestProxyDaoImpl.class); /* (non-Javadoc) * @see edu.wisc.my.restproxy.dao.RestProxyDao#proxyRequest(edu.wisc.my.restproxy.ProxyRequestContext) */ @Override - public Object proxyRequest(ProxyRequestContext context) { + public ResponseEntity proxyRequest(ProxyRequestContext context) { HttpHeaders headers = new HttpHeaders(); if(StringUtils.isNotBlank(context.getUsername()) && null != context.getPassword()) { StringBuffer credsBuffer = new StringBuffer(context.getUsername()); @@ -54,12 +65,10 @@ public Object proxyRequest(ProxyRequestContext context) { } HttpEntity request = context.getRequestBody() == null ? new HttpEntity(headers) : new HttpEntity(context.getRequestBody().getBody(), headers); - ResponseEntity response = restTemplate.exchange(context.getUri(), - context.getHttpMethod(), request, Object.class, context.getAttributes()); + context.getHttpMethod(), request, Object.class, context.getAttributes()); logger.trace("completed request for {}, response= {}", context, response); - Object responseBody = response.getBody(); - return responseBody; + return response; } } diff --git a/src/main/java/edu/wisc/my/restproxy/dao/RestProxyResponseErrorHandler.java b/src/main/java/edu/wisc/my/restproxy/dao/RestProxyResponseErrorHandler.java new file mode 100644 index 0000000..953ac4b --- /dev/null +++ b/src/main/java/edu/wisc/my/restproxy/dao/RestProxyResponseErrorHandler.java @@ -0,0 +1,43 @@ +/** + * + */ +package edu.wisc.my.restproxy.dao; + +import java.io.IOException; + +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.web.client.ResponseErrorHandler; + +/** + * {@link ResponseErrorHandler} implementation that does nothing and considers every + * {@link ClientHttpRequest} a success. + * + * This class does nothing because RestProxy is responsible soley for relaying requests and + * responses. We don't care what's in the response, we just forward it on. It's up to the client to deal + * with responses, whether they're successes or errors. + * + * @author Collin Cudd + */ +public class RestProxyResponseErrorHandler implements ResponseErrorHandler { + + /* + * (non-Javadoc) + * + * @see + * org.springframework.web.client.ResponseErrorHandler#handleError(org.springframework.http.client + * .ClientHttpResponse) + */ + @Override + public void handleError(ClientHttpResponse response) throws IOException { + //no-op + } + + /* (non-Javadoc) + * @see org.springframework.web.client.ResponseErrorHandler#hasError(org.springframework.http.client.ClientHttpResponse) + */ + @Override + public boolean hasError(ClientHttpResponse response) throws IOException { + return false; + } +} diff --git a/src/main/java/edu/wisc/my/restproxy/service/RestProxyService.java b/src/main/java/edu/wisc/my/restproxy/service/RestProxyService.java index 9b9f308..5e26673 100644 --- a/src/main/java/edu/wisc/my/restproxy/service/RestProxyService.java +++ b/src/main/java/edu/wisc/my/restproxy/service/RestProxyService.java @@ -5,6 +5,8 @@ import javax.servlet.http.HttpServletRequest; +import org.springframework.http.ResponseEntity; + import edu.wisc.my.restproxy.ProxyRequestContext; /** @@ -19,7 +21,7 @@ public interface RestProxyService { * * @param resourceKey * @param request - * @return the Object returned from the REST API, serialized to an {@link Object} (may return null) + * @return the {@link ResponseEntity} returned from the REST API (may return null) */ - public Object proxyRequest(String resourceKey, HttpServletRequest request); + public ResponseEntity proxyRequest(String resourceKey, HttpServletRequest request); } diff --git a/src/main/java/edu/wisc/my/restproxy/service/RestProxyServiceImpl.java b/src/main/java/edu/wisc/my/restproxy/service/RestProxyServiceImpl.java index 20ea371..4042da7 100644 --- a/src/main/java/edu/wisc/my/restproxy/service/RestProxyServiceImpl.java +++ b/src/main/java/edu/wisc/my/restproxy/service/RestProxyServiceImpl.java @@ -14,7 +14,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.util.FileCopyUtils; import org.springframework.web.servlet.HandlerMapping; @@ -62,7 +62,7 @@ void setEnv(Environment env) { * @see KeyUtils#getProxyHeaders(Environment, String, HttpServletRequest) */ @Override - public Object proxyRequest(final String resourceKey, final HttpServletRequest request) { + public ResponseEntity proxyRequest(final String resourceKey, final HttpServletRequest request) { final String resourceRoot = env.getProperty(resourceKey + ".uri"); if(StringUtils.isBlank(resourceRoot)) { logger.info("unknown resourceKey {}", resourceKey); @@ -95,11 +95,15 @@ public Object proxyRequest(final String resourceKey, final HttpServletRequest re RequestBody requestBody = null; try { InputStream inputStream = request.getInputStream(); - final String contentType = request.getHeader(HttpHeaders.CONTENT_TYPE); - if(inputStream != null && MediaType.APPLICATION_JSON_VALUE.equals(contentType)) { + final String contentType = request.getContentType(); + int contentLength = request.getContentLength(); + if(inputStream != null && contentLength > 0) { requestBody = new RequestBody() - .setBody(FileCopyUtils.copyToByteArray(inputStream)) - .setContentType(contentType); + .setBody(FileCopyUtils.copyToByteArray(inputStream)); + + if(StringUtils.isNotBlank(contentType)) { + requestBody.setContentType(contentType); + } context.setRequestBody(requestBody) .getHeaders().put(HttpHeaders.CONTENT_TYPE, contentType); diff --git a/src/main/java/edu/wisc/my/restproxy/web/ResourceProxyController.java b/src/main/java/edu/wisc/my/restproxy/web/ResourceProxyController.java index 407e2ad..3d7ea22 100644 --- a/src/main/java/edu/wisc/my/restproxy/web/ResourceProxyController.java +++ b/src/main/java/edu/wisc/my/restproxy/web/ResourceProxyController.java @@ -8,6 +8,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @@ -20,7 +23,7 @@ * * @author Nicholas Blair */ -@RestController +@Controller public class ResourceProxyController { @Autowired @@ -34,19 +37,27 @@ public class ResourceProxyController { void setEnv(Environment env) { this.env = env; } + /** + * Proxies the request and then calls {@link HttpServletResponse#setStatus(int)} with the + * {@link HttpStatus} recieved. If the proxy response contains content it's simply returned here + * as an {@link Object}. * + * @param request + * @param response + * @param key + * @return the body of the proxy response or null. */ @RequestMapping("/{key}/**") - public @ResponseBody Object proxyResource(HttpServletRequest request, + public @ResponseBody Object proxyResource(HttpServletRequest request, HttpServletResponse response, @PathVariable String key) { - Object result = proxyService.proxyRequest(key, request); - if(result == null) { - response.setStatus(HttpServletResponse.SC_NOT_FOUND); + ResponseEntity responseEntity = proxyService.proxyRequest(key, request); + if(responseEntity == null || responseEntity.getStatusCode() == null) { + response.setStatus(HttpStatus.NOT_FOUND.value()); return null; - } else { - return result; } + response.setStatus(responseEntity.getStatusCode().value()); + return responseEntity.hasBody() ? responseEntity.getBody() : null; } } diff --git a/src/test/java/edu/wisc/my/restproxy/ValidationResult.java b/src/test/java/edu/wisc/my/restproxy/ValidationResult.java new file mode 100644 index 0000000..e93a91e --- /dev/null +++ b/src/test/java/edu/wisc/my/restproxy/ValidationResult.java @@ -0,0 +1,23 @@ +/** + * + */ +package edu.wisc.my.restproxy; + +/** + * Just an example of an object that could be retruned by rest api. + * @author Collin Cudd + */ +public class ValidationResult { + + boolean success; + String message; + /** + * @param success + * @param message + */ + public ValidationResult(boolean success, String message) { + this.success = success; + this.message = message; + } + +} diff --git a/src/test/java/edu/wisc/my/restproxy/dao/RestProxyDaoImplTest.java b/src/test/java/edu/wisc/my/restproxy/dao/RestProxyDaoImplTest.java index 779a897..f30cc8f 100644 --- a/src/test/java/edu/wisc/my/restproxy/dao/RestProxyDaoImplTest.java +++ b/src/test/java/edu/wisc/my/restproxy/dao/RestProxyDaoImplTest.java @@ -74,9 +74,6 @@ public void proxyRequest_control() { headers.add("Authorization", "Basic " + base64Creds); HttpEntity expectedRequest = new HttpEntity(headers); - Object expectedResponseBody = Mockito.mock(Object.class); - Mockito.when(expectedResponse.getBody()).thenReturn(expectedResponseBody); - Mockito.when( restTemplate.exchange( Matchers.eq(context.getUri()), @@ -95,6 +92,6 @@ public void proxyRequest_control() { Matchers.eq(Object.class), Matchers.eq(context.getAttributes()) ); - assertEquals(expectedResponseBody, proxyResponse); + assertEquals(expectedResponse, proxyResponse); } } diff --git a/src/test/java/edu/wisc/my/restproxy/service/RestProxyServiceImplTest.java b/src/test/java/edu/wisc/my/restproxy/service/RestProxyServiceImplTest.java index ebb64e2..fb77cd6 100644 --- a/src/test/java/edu/wisc/my/restproxy/service/RestProxyServiceImplTest.java +++ b/src/test/java/edu/wisc/my/restproxy/service/RestProxyServiceImplTest.java @@ -16,11 +16,14 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.web.servlet.HandlerMapping; import edu.wisc.my.restproxy.ProxyRequestContext; +import edu.wisc.my.restproxy.ValidationResult; import edu.wisc.my.restproxy.dao.RestProxyDao; /** @@ -45,7 +48,7 @@ public void setup() { */ @Test public void proxyRequest_control() { - final Object result = new Object(); + final ResponseEntity result = new ResponseEntity(new Object(), HttpStatus.OK); MockHttpServletRequest request = new MockHttpServletRequest(); request.setMethod("GET"); @@ -59,13 +62,34 @@ public void proxyRequest_control() { assertEquals(result, proxy.proxyRequest("control", request)); } + /** + * Test simulates a proxy request which fails with a http 400 error. + * An error like this could be encountered if you were to post invalid data to a form. + * Test verifies the HttpStatus AND the body (which likely contins validation error message) are passed back to the client. + */ + @Test + public void proxyRequest_failsWithBadRequest() { + ValidationResult validationFailure = new ValidationResult(false, "you didn't check the box"); + final ResponseEntity expectedResult = new ResponseEntity(validationFailure, HttpStatus.BAD_REQUEST); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/control/foo"); + request.setContent("{ \"hello\": \"world\" }".getBytes()); + request.addHeader("Content-Type", "application/json"); + env.setProperty("control.uri", "http://destination"); + when(proxyDao.proxyRequest(any(ProxyRequestContext.class))).thenReturn(expectedResult); + ResponseEntity result = proxy.proxyRequest("control", request); + assertEquals(expectedResult, result); + assertEquals(validationFailure, result.getBody()); + } + /** * Experiment for {@link RestProxyServiceImpl#proxyRequest(String, HttpServletRequest)}, confirms * expected behavior when the configuration contains credentials. */ @Test public void proxyRequest_withCredentials() { - final Object result = new Object(); + final ResponseEntity result = new ResponseEntity(new Object(), HttpStatus.OK); MockHttpServletRequest request = new MockHttpServletRequest(); request.setMethod("GET"); @@ -85,7 +109,7 @@ public void proxyRequest_withCredentials() { */ @Test public void proxyRequest_withAdditionalHeader() { - final Object result = new Object(); + final ResponseEntity result = new ResponseEntity(new Object(), HttpStatus.OK); MockHttpServletRequest request = new MockHttpServletRequest(); request.setAttribute("wiscedupvi", "UW111A111"); @@ -105,7 +129,7 @@ public void proxyRequest_withAdditionalHeader() { */ @Test public void proxyRequest_withAdditionalHeaders_andPlaceholders() { - final Object result = new Object(); + final ResponseEntity result = new ResponseEntity(new Object(), HttpStatus.OK); MockHttpServletRequest request = new MockHttpServletRequest(); request.setAttribute("wiscedupvi", "UW111A111"); @@ -125,7 +149,7 @@ public void proxyRequest_withAdditionalHeaders_andPlaceholders() { */ @Test public void proxyRequest_withAdditionalPath() { - final Object result = new Object(); + final ResponseEntity result = new ResponseEntity(new Object(), HttpStatus.OK); MockHttpServletRequest request = new MockHttpServletRequest(); request.setMethod("GET"); diff --git a/src/test/java/edu/wisc/my/restproxy/web/ResourceProxyControllerTest.java b/src/test/java/edu/wisc/my/restproxy/web/ResourceProxyControllerTest.java index 13316a6..deccc5c 100644 --- a/src/test/java/edu/wisc/my/restproxy/web/ResourceProxyControllerTest.java +++ b/src/test/java/edu/wisc/my/restproxy/web/ResourceProxyControllerTest.java @@ -16,10 +16,13 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import edu.wisc.my.restproxy.ValidationResult; import edu.wisc.my.restproxy.service.RestProxyService; /** @@ -56,16 +59,36 @@ public void proxyResource_keyNotFound() { */ @Test public void proxyResource_control() { - Object result = new Object(); + final ResponseEntity result = new ResponseEntity(new Object(), HttpStatus.OK); final String resourceKey = "something"; - - HttpServletRequest request = new MockHttpServletRequest(); - when(service.proxyRequest(resourceKey, request)).thenReturn(result); - env.setProperty("something.uri", "http://localhost/something"); + HttpServletRequest request = new MockHttpServletRequest(); MockHttpServletResponse response = new MockHttpServletResponse(); + when(service.proxyRequest(resourceKey, request)).thenReturn(result); - assertEquals(result, controller.proxyResource(request, response, "something")); + assertEquals(result.getBody(), controller.proxyResource(request, response, "something")); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals(HttpStatus.OK.value(), response.getStatus()); + } + + /** + * Simulates a call to {@link ResourceProxyController#proxyResource(HttpServletRequest, HttpServletResponse, String)} encountering a 400. + * Test verifies the httpstatus and body (a {@link ValidationResult} in this case) are passed back to the client. + */ + @Test + public void proxyResource_badRequest() { + ValidationResult validationFailure = new ValidationResult(false, "you didn't check the box"); + final ResponseEntity expected = new ResponseEntity(validationFailure, HttpStatus.BAD_REQUEST); + final String resourceKey = "something"; + HttpServletRequest request = new MockHttpServletRequest(); + when(service.proxyRequest(resourceKey, request)).thenReturn(expected); + + env.setProperty("something.uri", "http://localhost/something"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + assertEquals(expected.getBody(), controller.proxyResource(request, response, "something")); + assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatus()); } }