Last active
August 29, 2015 14:03
-
-
Save dwelch2344/b21ac7a0dd84e90ea57c to your computer and use it in GitHub Desktop.
An example of BatchController's request and responses.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@RestController | |
@RequestMapping("/$batch") | |
public class BatchRequestController { | |
@Inject | |
private List<RequestMappingHandlerMapping> mappings; | |
@Inject | |
private List<HandlerAdapter> handlerAdapters; | |
@Inject | |
private ObjectMapper jackson; | |
@Inject | |
private SecurityService securityService; | |
@RequestMapping(value="", method= RequestMethod.POST) | |
public Object get(HttpServletRequest realRequest, @RequestBody BatchRequests requests) throws Exception { | |
requests.scrubContext( realRequest.getContextPath() ); | |
BatchRequestProcessor processor = new BatchRequestProcessor(mappings, handlerAdapters, jackson){ | |
@Override | |
public Object unwrapResult(Object result) { | |
if( result instanceof ResponseEntity){ | |
ResponseEntity re = ((ResponseEntity) result); | |
Object body = re.getBody(); | |
if( body instanceof Page){ | |
Page page = (Page) body; | |
return page.getContent(); | |
} | |
} | |
return super.unwrapResult(result); | |
} | |
}; | |
processor.processRequests( requests ); | |
return processor.getResponses(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@RequiredArgsConstructor | |
public class BatchRequestProcessor{ | |
private static final Pattern URL_ID_PATTERN = Pattern.compile("^\\$([a-zA-Z][a-zA-Z0-9]*).*"); | |
@Getter | |
private final BatchResponses responses = new BatchResponses(); | |
private final List<RequestMappingHandlerMapping> mappings; | |
private final List<HandlerAdapter> handlerAdapters; | |
private final ObjectMapper jackson; | |
public void processRequest(BatchRequests.Request r){ | |
// fail fast if a dependency failed | |
if( r.hasDependencies() ){ | |
for(String dependsOn : r.getDependsOn() ){ | |
if( !responses.didSucceed(dependsOn) ){ | |
// TODO add a payload object to give more info on what failed | |
responses.add(577, r.getId(), new BatchResponseError("A required request did not succeed.", "DEP-FAILED"), null); | |
return; | |
} | |
} | |
if( r.hasPreprocessors() ){ | |
preprocess(r); | |
} | |
} | |
try{ | |
String url = processUrl(r.getUrl()); | |
// create our mock request / response | |
MockHttpServletRequest req = new MockHttpServletRequest( r.getMethod().toString(), url); | |
MockHttpServletResponse res = new MockHttpServletResponse(); | |
// handle passing the request payload downstream if needed | |
serializePayload(r, req); | |
// loop our mappings and find a handler for the request | |
Object rawResult = null; | |
boolean handled = false; | |
for(RequestMappingHandlerMapping mapping : mappings){ | |
try{ | |
HandlerExecutionChain chain = mapping.getHandler(req); | |
if( chain != null ){ | |
Object handler = chain.getHandler(); | |
for( HandlerAdapter adapter : handlerAdapters ){ | |
if( adapter.supports(handler) ){ | |
Assert.isAssignable(RequestMappingHandlerAdapter.class, adapter.getClass(), "Right now we only support RequestMappingHandlerAdapter handlers"); | |
RequestMappingHandlerAdapter rmha = (RequestMappingHandlerAdapter) adapter; | |
rawResult = processRequest(req, res, rmha, handler); | |
if( !verifyValidResponse(res) ){ | |
// TODO do we didn't get a valid response. send an error | |
} | |
handled = true; | |
break; | |
} | |
} | |
if( handled){ | |
break; | |
} | |
throw new IllegalStateException("Could not find an adapter to process this request"); | |
} | |
// no request mapping, so 404 this request | |
res.setStatus(404); | |
}catch(HttpRequestMethodNotSupportedException e){ | |
// mapping.getHandler(req) throws this exception if the request path / payload is valid, but the http method is not | |
res.setStatus(500); | |
rawResult = new BatchResponseError(e.getMessage(), "INVALID-HTTP-METHOD"); | |
break; | |
} | |
} | |
generateResponse(r, res, rawResult); | |
}catch(Exception e){ | |
throw new RuntimeException("Failed processing", e); | |
} | |
} | |
@SneakyThrows | |
public void serializePayload(BatchRequests.Request r, MockHttpServletRequest req) { | |
if( r.hasPayload() ){ | |
String json = jackson.writeValueAsString(r.getPayload()); | |
req.setContent( json.getBytes() ); | |
req.setContentType("application/json"); | |
} | |
} | |
/** | |
* @see org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle(org.springframework.web.context.request.ServletWebRequest, org.springframework.web.method.support.ModelAndViewContainer, Object...) | |
*/ | |
@SneakyThrows | |
private Object processRequest(MockHttpServletRequest req, MockHttpServletResponse res, RequestMappingHandlerAdapter adapter, Object handler) { | |
ServletInvocableHandlerMethod requestMethod = createHandlerMapping(adapter, (HandlerMethod) handler); | |
// invoke necessary methods including processing of the HttpServletResponse | |
ServletWebRequest webRequest = new ServletWebRequest(req, res); | |
ModelAndViewContainer mav = new ModelAndViewContainer(); | |
Object result = requestMethod.invokeForRequest(webRequest, mav); | |
// Now trigger the returnValueHandlers (so our HttpServletRequest has the proper headers set and what not) | |
HandlerMethodReturnValueHandlerComposite returnValueHandlers = new HandlerMethodReturnValueHandlerComposite(); | |
returnValueHandlers.addHandlers(adapter.getReturnValueHandlers()); | |
if( result == null && requestMethod.getReturnType().getParameterType().isAssignableFrom(HttpStatus.class) ){ | |
result = new HashMap<String, String>(){{ | |
put("message", "OK"); | |
}}; | |
} | |
returnValueHandlers.handleReturnValue(result, requestMethod.getReturnValueType(result), mav, webRequest); | |
return unwrapResult(result); | |
} | |
/** | |
* @see RequestMappingHandlerAdapter#createRequestMappingMethod(org.springframework.web.method.HandlerMethod, org.springframework.web.bind.support.WebDataBinderFactory) | |
*/ | |
public ServletInvocableHandlerMethod createHandlerMapping(RequestMappingHandlerAdapter adapter, HandlerMethod handlerMethod) { | |
Method bdMethod = ReflectionUtils.findMethod(adapter.getClass(), "getDataBinderFactory", HandlerMethod.class); | |
ReflectionUtils.makeAccessible(bdMethod); | |
Object binderFactory = ReflectionUtils.invokeMethod(bdMethod, adapter, handlerMethod); | |
Method method = ReflectionUtils.findMethod(adapter.getClass(), "createRequestMappingMethod", HandlerMethod.class, WebDataBinderFactory.class); | |
ReflectionUtils.makeAccessible(method); | |
return (ServletInvocableHandlerMethod) ReflectionUtils.invokeMethod(method, adapter, handlerMethod, binderFactory); | |
} | |
public Object unwrapResult(Object result) { | |
// finally, unwrap our Controller's object if needed | |
if( result instanceof ResponseEntity){ | |
ResponseEntity re = ((ResponseEntity) result); | |
Object body = re.getBody(); | |
Assert.isAssignable(Resource.class, body.getClass()); | |
return ((Resource) body).getContent(); | |
} | |
// TODO handle mavs? | |
return result; | |
} | |
/** | |
* Creates a SpEL context with the current request, as well as any previous requests that were depended on. | |
* | |
* So, if we had Requests A, B, and C, where C depended on A and had preprocessors, C would have the following variables: | |
* | |
* 'request' - the BatchRequests.Request object for the request being processed | |
* 'A' - The RESULT payload from A's request. | |
* | |
* Note that if A failed, C would never run (thus a dependsOn payload can be implicitly depended on) | |
* | |
*/ | |
public void preprocess(BatchRequests.Request request) { | |
// create a mapping of all the requests that our user might depend on | |
Map<String, Object> vars = Maps.newHashMap(); | |
vars.put("request", request); | |
for(String dependsOn : request.getDependsOn()){ | |
vars.put( dependsOn, responses.get(dependsOn).getPayload() ); | |
} | |
// create our parser and context | |
ExpressionParser parser = new SpelExpressionParser(); | |
StandardEvaluationContext context = new StandardEvaluationContext(vars); | |
context.addPropertyAccessor(new MapAccessor()); | |
for(BatchRequests.BatchCommand cmd : request.getPreprocessors()){ | |
Object value = parser.parseExpression(cmd.getExpression()).getValue(context); | |
parser.parseExpression(cmd.getTarget()).setValue(context, value); | |
} | |
} | |
public void generateResponse(BatchRequests.Request r, MockHttpServletResponse res, Object payload) { | |
String id = r.getId(); | |
int status = res.getStatus(); | |
MultiValueMap<String, String> headers = extractHeaders(res); | |
responses.add(status, id, payload, headers); | |
} | |
// looks up url, replacing curly-brace lookups for created entities | |
public String processUrl(String url) { | |
if( url.startsWith("$") ){ | |
Matcher matcher = URL_ID_PATTERN.matcher(url); | |
if( matcher.find() ){ | |
String identifier = matcher.group(1); | |
String href = getEntityUrl(identifier); | |
url = href + url.substring( identifier.length() + 1); | |
} | |
} | |
return url; | |
} | |
// meant for extension | |
public MultiValueMap<String, String> extractHeaders(MockHttpServletResponse res) { | |
MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>(); | |
for( String name : res.getHeaderNames() ){ | |
if( ApiResourceBaseController.X_SELF_HREF.equals(name) ){ | |
headers.add( name, (String) res.getHeaderValue(name)); | |
} | |
} | |
return headers; | |
} | |
public boolean verifyValidResponse(MockHttpServletResponse res) { | |
// TODO make sure it's text based and other stuff. | |
return true; | |
} | |
public String getEntityUrl(String requestId){ | |
BatchResponses.Response response = responses.get(requestId); | |
Assert.notNull(response, "No response by keyed by " + requestId); | |
return response.getHeaders().getFirst( ApiResourceBaseController.X_SELF_HREF ); | |
} | |
public void processRequests(BatchRequests requests) { | |
for(BatchRequests.Request r : requests.getRequests()){ | |
processRequest(r); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"requests": [ | |
{ | |
"id": "A", | |
"url": "/api/companies", | |
"method": "POST", | |
"payload": { | |
"name" : "Some Company Name" | |
} | |
}, | |
{ | |
"id": "ShouldFail", | |
"dependsOn": ["A"], | |
"url": "$A/foobar", | |
"method": "POST", | |
"payload": { | |
"message": "Yeah, this ain't gonna end well" | |
} | |
}, | |
{ | |
"id": "B", | |
"dependsOn" : ["A"], | |
"url": "/api/companies", | |
"method": "POST", | |
"payload":{ | |
"name": "A temporary name you should never see" | |
}, | |
"preprocessors": [ | |
{ | |
"target": "request.payload.name", | |
"expression": "\"Created after \" + A.name" | |
} | |
] | |
} | |
] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"responses": [ | |
{ | |
"status": 201, | |
"id": "A", | |
"payload": { | |
"id": 75, | |
"name": "Some Company Name" | |
}, | |
"headers": { | |
"X-SELF-HREF": [ | |
"http://localhost:8080/api/companies/75" | |
] | |
} | |
}, | |
{ | |
"status": 404, | |
"id": "ShouldFail", | |
"headers": {} | |
}, | |
{ | |
"status": 201, | |
"id": "B", | |
"payload": { | |
"id": 76, | |
"name": "Created after Some Company Name" | |
}, | |
"headers": { | |
"X-SELF-HREF": [ | |
"http://localhost:8080/api/companies/76" | |
] | |
} | |
} | |
] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment