Skip to content

Instantly share code, notes, and snippets.

@dwelch2344
Last active August 29, 2015 14:03
Show Gist options
  • Save dwelch2344/b21ac7a0dd84e90ea57c to your computer and use it in GitHub Desktop.
Save dwelch2344/b21ac7a0dd84e90ea57c to your computer and use it in GitHub Desktop.
An example of BatchController's request and responses.
@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();
}
}
@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);
}
}
}
{
"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"
}
]
}
]
}
{
"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