Spring Framework
  1. Spring Framework
  2. SPR-9980

Make @RequestMapping inject the negotiated MediaType

    Details

    • Type: Improvement Improvement
    • Status: Resolved
    • Priority: Major Major
    • Resolution: Won't Fix
    • Affects Version/s: 3.1.3
    • Fix Version/s: None
    • Component/s: Web

      Description

      I have a use caes where I do return a suitable object according for the negotiated content type.
      In my case I accept JSON and XML. When JSON is returned to the client I produce a Map, when XML is returned I produce a custom object.

      There is no way to determine easily which content type will be written out to the client. With examining the Accept header and iterating over registered message converters.

      I have a work around which looks like this:

      @RequestMapping(value = "/{project:[A-Z0-9_+\\.\\(\\)=\\-]+}", method = RequestMethod.GET, 
        produces = { MediaType.APPLICATION_XML_VALUE })
        public ResponseEntity<Object> lookupProjectAsXml(@PathVariable String project,
            @RequestParam(value = "fields", required = false) String fields) {
      
          String[] fieldsArray = StringUtils.split(fields, ',');
      
          return lookup(project, fieldsArray, false, MediaType.APPLICATION_XML);
        }
      

      and

      
      @RequestMapping(value = "/{project:[A-Z0-9_+\\.\\(\\)=\\-]+}", method = RequestMethod.GET, 
        produces = { MediaType.APPLICATION_JSON_VALUE })
        public ResponseEntity<Object> lookupProjectAsJson(@PathVariable String project,
            @RequestParam(value = "fields", required = false) String fields,
            @RequestParam(value = "asList", required = false, defaultValue = "false") boolean asList) {
      
          String[] fieldsArray = StringUtils.split(fields, ',');
      
          return lookup(project, fieldsArray, asList, MediaType.APPLICATION_JSON);
        }
      

      I would rather have:

      @RequestMapping(value = "/{project:[A-Z0-9_+\\.\\(\\)=\\-]+}", method = RequestMethod.GET, 
        produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE })
        public ResponseEntity<Object> lookupProject(@PathVariable String project,
            @RequestParam(value = "fields", required = false) String fields,
            @RequestParam(value = "asList", required = false, defaultValue = "false") boolean asList,
            MediaType mediaType) {
      
          // Process request...
          
          Object body;
          if (mediaType.equals(MediaType.APPLICATION_JSON)) {
            body = projectValues;
          } else if (mediaType.equals(MediaType.APPLICATION_XML)) {
            body = new Project(projectValues);
          } else {
            throw new NotImplementedException("Project lookup is not implemented for media type '" + mediaType + "'");
          }
        
          return new ResponseEntity<Object>(body, HttpStatus.OK);
        }
      

        Issue Links

          Activity

          Hide
          Rossen Stoyanchev added a comment - - edited

          The "negotiated MediaType" is not yet available at the time of controller method invocation. The actual content negotiation is in HttpEntityMethodProcessor, which handles the returned ResponseEntity. What we do know is the requested media types and in 3.2 that is done through a ContentNegotiationManager so it would be easy to create a HandlerMethodArgumentResolver that supports List<MediaType>.

          That said it seems like it would be nicer, in your example, if we provided an option to post-process the return value before it is written to the response. This would help @ResponseBody and ResponseEntity methods since they can't use a HandlerInterceptor to do that. That way you maybe wouldn't have to have an if-else in every method that needs this. Instead you would return the same value and wrap it for XML. It would also help with SPR-9226.

          What do you think?

          Show
          Rossen Stoyanchev added a comment - - edited The "negotiated MediaType" is not yet available at the time of controller method invocation. The actual content negotiation is in HttpEntityMethodProcessor , which handles the returned ResponseEntity . What we do know is the requested media types and in 3.2 that is done through a ContentNegotiationManager so it would be easy to create a HandlerMethodArgumentResolver that supports List<MediaType> . That said it seems like it would be nicer, in your example, if we provided an option to post-process the return value before it is written to the response. This would help @ResponseBody and ResponseEntity methods since they can't use a HandlerInterceptor to do that. That way you maybe wouldn't have to have an if-else in every method that needs this. Instead you would return the same value and wrap it for XML. It would also help with SPR-9226 . What do you think?
          Hide
          Michael Osipov added a comment -

          Rossen,

          to the first paragraph: Yes, that would be nice to have a list of MediaType which are supported by the client. Would not solve my issue but may be handy for other purposes. Anyway why is MediaType not an Enum?

          to the second paragraph: This sounds good, one would move the if-else-clause to a single spot. But how would I know that, for instance the map from that exact method needs to be wrapped. Would this be per-controller just like a @ExceptionHandler?

          Show
          Michael Osipov added a comment - Rossen, to the first paragraph: Yes, that would be nice to have a list of MediaType which are supported by the client. Would not solve my issue but may be handy for other purposes. Anyway why is MediaType not an Enum? to the second paragraph: This sounds good, one would move the if-else-clause to a single spot. But how would I know that, for instance the map from that exact method needs to be wrapped. Would this be per-controller just like a @ExceptionHandler?
          Hide
          Rossen Stoyanchev added a comment - - edited

          Anyway why is MediaType not an Enum?

          It's not quite an enum since you have wildcard types and subtypes, vendor extensions, XML or JSON compatible times (i.e. "*+json"), not to mention parameters like quality or charset and others.

          But how would I know that, for instance the map from that exact method needs to be wrapped. Would this be per-controller just like a @ExceptionHandler?

          This is something that would be registered globally. You would know the return type is ResponseEntity and you would be able to check the requested media type or the "producible" media type in order to decide.

          Show
          Rossen Stoyanchev added a comment - - edited Anyway why is MediaType not an Enum? It's not quite an enum since you have wildcard types and subtypes, vendor extensions, XML or JSON compatible times (i.e. "*+json"), not to mention parameters like quality or charset and others. But how would I know that, for instance the map from that exact method needs to be wrapped. Would this be per-controller just like a @ExceptionHandler? This is something that would be registered globally. You would know the return type is ResponseEntity and you would be able to check the requested media type or the "producible" media type in order to decide.
          Hide
          Michael Osipov added a comment -

          But how would I know that, for instance the map from that exact method needs to be wrapped. Would this be per-controller just like a @ExceptionHandler?

          This is something that would be registered globally. You would know the return type is ResponseEntity and you would be able to check the requested media type or the "producible" media type in order to decide.

          If you have to register this globally how is one supposed to know in this handler that for instance a simple (and very common) object like Map must be wrapped in X where X can be something completely different based on the controller which is returning the Map?

          Show
          Michael Osipov added a comment - But how would I know that, for instance the map from that exact method needs to be wrapped. Would this be per-controller just like a @ExceptionHandler? This is something that would be registered globally. You would know the return type is ResponseEntity and you would be able to check the requested media type or the "producible" media type in order to decide. If you have to register this globally how is one supposed to know in this handler that for instance a simple (and very common) object like Map must be wrapped in X where X can be something completely different based on the controller which is returning the Map ?
          Hide
          Rossen Stoyanchev added a comment -

          In addition to the return type you would also have access to a MethodParameter instance from which you can get the declaring class (i.e. the controller) as well as the method and any annotations it has. See the HandlerMethodReturnValueHandler. It shows what would be available roughly.

          Show
          Rossen Stoyanchev added a comment - In addition to the return type you would also have access to a MethodParameter instance from which you can get the declaring class (i.e. the controller) as well as the method and any annotations it has. See the HandlerMethodReturnValueHandler . It shows what would be available roughly.
          Hide
          Michael Osipov added a comment -

          I am missing a link here. Is there something already where I could plugin such a thing than wrap my map in a custom object for that specific controller?

          Show
          Michael Osipov added a comment - I am missing a link here. Is there something already where I could plugin such a thing than wrap my map in a custom object for that specific controller?
          Hide
          Arnaud Cogoluègnes added a comment -

          pull request for the list of requested media types as a method argument: https://github.com/SpringSource/spring-framework/pull/232

          Show
          Arnaud Cogoluègnes added a comment - pull request for the list of requested media types as a method argument: https://github.com/SpringSource/spring-framework/pull/232
          Hide
          Rossen Stoyanchev added a comment -

          Moving to general backlog now. See discussion under pull request for further details.

          Show
          Rossen Stoyanchev added a comment - Moving to general backlog now. See discussion under pull request for further details.
          Hide
          Rossen Stoyanchev added a comment -

          Resolving as "Won't fix". See pull request for further comments.

          Show
          Rossen Stoyanchev added a comment - Resolving as "Won't fix". See pull request for further comments.

            People

            • Assignee:
              Rossen Stoyanchev
              Reporter:
              Michael Osipov
              Last updater:
              Rossen Stoyanchev
            • Votes:
              0 Vote for this issue
              Watchers:
              4 Start watching this issue

              Dates

              • Created:
                Updated:
                Resolved:
                Days since last comment:
                37 weeks, 5 days ago