-
Notifications
You must be signed in to change notification settings - Fork 38.5k
True Content-Negotiation [SPR-13679] #18254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Rossen Stoyanchev commented Content negotiation is about clients being able to request different representations of a resource (see wikipedia for example). It has nothing to do with the input which is fixed and there is nothing to be negotiated (it is what it is). So your question can be split into two -- one processing different kinds of input and two serving different representations of a resource.
|
Enrique Ruiz (DiSiD) commented Hi Rossen, A common Use-Case is the multi-device applications. In these cases the user access to the application via web browser from a PC and uses HTML forms to manage the domain entities, what it is also know as CRUD operations. Moreover the user has a native mobile application (Android, iOS, ...) to execute the same CRUD operations but via AJAX and using JSON messages. All remains the same, but we have to duplicate the control logic. On other side, at Spring Roo project we're working in the version 2.0 that will include several improvements as the generation of Spring Boot applications or the full separation of the Control-Layer from the View-Layer. The goal of the full separation of the C-L form the V-L is to let the developers can generate a full web app Technically the full separation of the C-L from the V-L implies that a handler method in the Controller must support any request and response. Note each view technology has its own way to call the server, so the support of several view technologies in Spring Roo means different handler methods should be generated in anticipation that the developer can chose the View technology, this could be avoided if Spring MVC would include the feature to chose the handler method, the |
Rossen Stoyanchev commented Thanks for the extra detail. For a Web browser application serving HTML forms the controller logic is different. A model is prepared with multiple attributes -- the form-backing object (yes) but also other objects to help populate drop-down boxes, etc. After the form is submitted, you wouldn't want to use @RequestMapping(path="/accounts", method=POST)
public String addAccount(@Valid @ModelAttribute Account account, Errors errors) {
if (errors.hasErrors) {
return "editAccount";
}
// Save the account
return "redirect:/accounts/{id}";
} In a REST API there is no need to populate a model with extra attributes. No need to re-display the form with errors. No need to redirect either on success. Rather you return 201 or 400 with error details in the response body. You can add things like HATEOAS as well here that don't apply to HTML-based apps. If you build an HTML-based Web application for humans, you can add the ContentNegotiatingViewResolver and that will allow you to get non-HTML representations for methods mapped to HTTP GET. However if also want to support a full-fledged REST API in addition I'd recommend a separate URL space, and separate controller logic. In summary it's really a two-way (and not a 4-way) split between controller logic Web apps with HTML forms for use by humans vs REST API for use by programmatic clients. |
Enrique Ruiz (DiSiD) commented Hi Rossen, It is true that the developers could want to apply the post-redirect-get pattern, so in that case they must to add a new handler method to apply it. But the web applications aren't composed by update/create forms only, the read and query actions are needed and in those cases the developers could have to duplicate the controller logic up to 4 times. I will try to explain myself better, please take a look to the article Content Negotiation using Spring MVC : Combining Data and Presentation Formats, what I'm proposing is an improvement to avoid the duplicated controller logic, as shown in this article. Let me simplify this example: @Controller
class AccountController {
@Autowired
protected AccountService accountService;
/**
* METHOD A: RESTful method
* TEST:
* curl -i -H 'Accept: application/json' http://localhost:8080/accounts?name=TheCustomerName
*/
@RequestMapping(value="/accounts", produces="application/json")
@ResponseStatus(HttpStatus.OK)
public @ResponseBody List<Account> listWithMarshalling(Customer customer) {
return accountService.findAllAccounts(customer.getName());
}
/**
* METHOD B: View-based method
* TEST:
* curl -i http://localhost:8080/accounts?name=TheCustomerName
*/
@RequestMapping("/accounts")
public String listWithView(Model model, Customer customer) {
// Call RESTful method to avoid repeating account lookup logic
model.addAttribute( listWithMarshalling(customer) );
// Return the view to use for rendering the response
return "accounts/list";
}
} These methods let you to handle the following requests:
But the requeriments to create web applications are many and various, for example, moreover the above use cases, if you would need to handle the requests below:
The handler methods should be something like this: @Controller
class AccountController {
...
/**
* METHOD C: RESTful method
* TEST:
* curl -i -H 'Content-Type: application/json' -H 'Accept: application/json' -d '{"name":"TheCustomerName"}' http://localhost:8080/accounts
*/
@RequestMapping(value="/accounts", produces="application/json", consumes="application/json")
@ResponseStatus(HttpStatus.OK)
public @ResponseBody List<Account> listWithMarshalling(@Valid @RequestBody Customer customer, BindingResult errors) {
return accountService.findAllAccounts(customer.getName());
}
/**
* METHOD D: View-based method
* TEST:
* curl -i -H 'Content-Type: application/json' -d '{"name":"TheCustomerName"}' http://localhost:8080/accounts
*/
@RequestMapping(value="/accounts", consumes="application/json")
public String listWithView(Model model, @Valid @RequestBody Customer customer, BindingResult errors) {
// Call RESTful method to avoid repeating account lookup logic
model.addAttribute( listWithMarshalling(customer) );
// Return the view to use for rendering the response
return "accounts/list";
}
} This is why I said you have to duplicate the controller logic up to 4 times. Note that in all cases, all remains identical: the data binding, the object validation, ... what it only changes is how the data is in the HTTP request and the message of the response. Rossen, note I think that Spring could manage all of them in only one controller method just with some info at the HTTP headers so I think it would be a great improvement that could be really useful for the developers. The reality is that the HTML-based Web applications for humans is not the trend, the reality and the trend is that the Web applications must be ready for both html-based interaction as for the RESTful API interaction. In terms of application functionality, the clients require the same features via web, via mobile applications and in general via full RESTful API. This issue propose to improve code maintenance when it can be achieved, as for example on the control logic to read and to query data. Finally, another use case is AngularJS which works via REST API and JSON messages, by having the proposed improvement a web application which View-Layer is based on JSP could be migrated to a V-L based on AngularJS easily, without haven't to modify the Controller-Layer. |
Rossen Stoyanchev commented
What do you mean by "HTTP parameters"? The Servlet API has request parameters, I'm guessing that's what you mean. Those can come from query parameters or from form data in the body of the request. I think what you mean is the latter which in turn means method A and B should have
I question the need for method D since HTML-based applications send form data. Unlike a REST API which needs to support different kinds of client implementations who may send different kinds of input, in an HTML-based application it's the application serving the HTML and generally having one kind of input (form data) is what makes sense. Rather than looking at all permutations of the programming model, from an HTTP perspective (nothing to do with Spring MVC) I see two main usage styles. One is HTML-centric and the other follows REST conventions. There are enough differences between the two that merit differences in the programming model. This is not just about the POST-REDIRECT-GET pattern but also about basic things like the status codes used, the way errors are handled, etc (I gave examples of these already). When it comes to POST-REDIRECT-GET it really should be the default way to do things in an HTML-centric approach, not just something you might want to do. |
Enrique Ruiz (DiSiD) commented Yes, I meant HTTP Query parameters and HTTP Form data. What I'm not sure is why you are centered in form data only ... but anyway.
IMHO Spring MVC has plenty of similar features that makes the live of the developer easier, as for example the Deeply I think you and me have different view of the current applications, you see different programming models one HTML-centric and other REST. I see the same programming model, applications with an high percentage of controller logic duplicated when you combine both. This issue is about improve Spring MVC to support Spring MVC applications that combines HTML with RESTful API in an easy way without having to duplicate the controller logic. But without loosing the possibility to customize each model by using the current way. Take a look and test this Spring Boot application that illustrates the problem. Please debug and run the request tests. All the best, |
Rossen Stoyanchev commented I focus on form data because I'm thinking of the HTTP POST and PUT, those are the key areas where you have differences in controller logic and response status codes. Getting a controller method for GET methods isn't hard. The example Spring Boot application does not make sense without further explanation. What is the "/accounts" endpoint? Is that a listing of accounts for a customer, in which case shouldn't there be something (like a path variable) that identifies the customer in the URL? It's not clear why identifying which customer has to come as JSON in the request body or require data binding? |
Enrique Ruiz (DiSiD) commented
Yes, but as I said in one of my previous comments, this proposal doesn't prevent the developer to customize the handler methods, for example for managing specific cases.
Well, it is just an example that illustrates the problem, I will try to explain further below.
The meaning of the "/accounts" follows the common RESTful pattern:
Note, in the given Spring Boot application I only implemented the methods to manage the searches for accounts with a certain It does not matter if the data comes as request parameter or comes as path variable, the problem is exactly the same. But yes, it is true that for the read method, for example "/accounts/{number}", you will have 2 duplicated controller methods only.
IMHO is the best way to avoid duplicated conversion logic. It does not matter from where the data comes: search form, AJAX search, ... there is no need to have methods like this: @RequestMapping("/accounts")
public String listWithView(Model model, @RequestParam String customerName,
@RequestParam @DateTimeFormat(pattern="MM/dd/yyyy")
Date customerBirthdate,
...) {
} Just set up the |
Rossen Stoyanchev commented Since I do not understand what you're trying to do in general, I will simply respond with this commit that reduces your example two controller methods, one accepting input from request parameters and the other from the request body (plus the ContentNegotiatingViewResolver config): @RequestMapping(value="/accounts")
public String listWithRequestParams(Customer customer, Model model) {
return listInternal(customer, model);
}
@RequestMapping(value="/accounts", consumes="application/json")
public String listWithRequestBody(@RequestBody Customer customer, Model model) {
return listInternal(customer, model);
} What's not clear is how each of these scenarios will be used from the client side? For example I can understand the reason for request parameter input with HTML output as well as JSON input and output. What's making use of request parameter input + JSON output? That sounds like a REST or Ajax call based on how it deals with the response in which case why not expect JSON input there which also sets up the right expectations for how to report errors (i.e. for consumption by programmatic client)? Same question for JSON input with HTML output. Based on the response it sounds like a response for rendering in a browser in which case there would be a search form presumably? |
Enrique Ruiz (DiSiD) commented
Several advanced UI components use this way. Note that in these cases a developer cannot decide to use JSON input message or input request parameters, the API of the UI components is who set the data input format. Just an example, one of the best jQuery plugins for managing tables is Datatables. Take a look to this Datatables example, enter text in the Search input and you will see how the filtering data is sent as HTTP query parameters and the response is given in a JSON message. Another datatables example, enter the same text in the Search input and you will see how the filtering data is sent as HTTP form data and the response is given in a JSON message too.
As an example take a look to the function jQuery .load(), it does an AJAX request and it expects an HTML message that it is rendered inside a container of the page. This pattern is applied in UI components too, for example, you take a look to the gvNIX petclinic demo create Pet form, the owner field is a search field, just press the loupe and you will see how the HTML response of an AJAX request is rendered inside a dialog component. |
Rossen Stoyanchev commented Okay that puts the discussion on much more concrete terms. So far what I see are examples of retrieving data for programmatic use by UI components in a browser. The 2 controller methods I showed above are the best (that I can think of) with out of the box behavior. For that note there is no duplication of controller logic. Going to a single controller method where the Customer object is initialized either from request parameters or from the request body is not that hard at all from a technical perspective (more on that shortly). It's more an issue of semantics. For once a Customer argument without annotations already has a set meaning that we can't all of a sudden change. More importantly though I think a clear separation of initialization via data binding from request params or through the request body is a a good (and intuitive) foundation. Not having such clear semantics would certainly lead to many surprises or impossible cases (e.g. both request body and request parameters). On top of this it's easy to create a custom annotation such as We could even consider providing such an annotation out of the box, if it seems to be generally useful. Creating your own in the mean time should be reasonably straight forward. |
Enrique Ruiz (DiSiD) commented
Ok, I understand ... I suppose this is the reason because a solution based on a new annotation is needed.
Yes, for avoiding this impossible cases, you could take in account the request header attribute "Content-Type", for having a criteria to decide what are the data that must be used to populate an entity (data-binding) when the request has a body and request parameters together.
I agree with your proposal.
Totally agree. In the decission to delegate to MAMP or RRBMP you could take in account the request header attribute "Content-Type", for example.
Thanks Rossen for your support and patience :) Rossen, IMHO the same improvement could be done for the response. For those methods that don't apply the POST-REDIRECT-GET pattern, a new annotation would be great with the combined semantics of |
Rossen Stoyanchev commented No problem. I've created #18315 as a concrete actionable item. As for alternating between |
Enrique Ruiz (DiSiD) opened SPR-13679 and commented
Spring MVC provides three ways to support Content-Negotiation but IMHO it isn't a true CN because really only one method in the
Controller
cannot handle several formats requested by the user.Due to the current CN depends on annotations in the handler methods a developer has to create 4 methods to support all the possible combinations:
@RequestParam
+@ResponseBody
: received data as parameters and returned data as a JSON message@RequestParam
+View
: received data as parameters and returned data as HTML@RequestBody
+@ResponseBody
: received data as JSON message and return message as a JSON message@RequestBody
+View
: received data as JSON message and return message as HTMLAlthough you can delegate between these methods you have to maintain 4 methods that potentially will change along the time and could evolve to different implementations, so really you are duplicating control logic.
What is true CN? For me true CN is to choose the conversion and binding of the incoming parameters or the conversion and binding of the request body depending on the HTTP "
Content-Type
" header property of the request.In the same way, the conversion and formatting of the model objects included in the response could be by using a
View
or aMessageConverter
depending on the HTTP "Accept
" header property of the request.Of course, the logical view-name should be infered from the URL automatically if needed, for example by using a
DefaultRequestToViewNameTranslator
.By doing that only one handler method is needed and we won't duplicate control logic. For example:
Affects: 4.2.2
Issue Links:
5 votes, 5 watchers
The text was updated successfully, but these errors were encountered: