I should probably point out that Spring is not in and of itself necessarily crucial to this question, but I encountered this behavior while using Spring, so the question uses the situation in Spring in which I encountered this.
I have a controller class that maps requests for GET and POST requests to the same set of URLs for a particular form. This form has different URLs for different locales, but there is only one method for the GET request, and one for the POST, since the logic at the controller level for the form is identical for each locale site (but things deeper in the logic, like locale-specific validation, may be different). Example:
#Controller
public class MyFormController {
// GET request
#RequestMapping(value={"/us-form.html", "/de-form.html", "/fr-form.html"},
method={RequestMethod.GET})
public String showMyForm() {
// Do some stuff like adding values to the model
return "my-form-view";
}
// POST request
#RequestMapping(value={"/us-form.html", "/de-form.html", "/fr-form.html"},
method={RequestMethod.POST})
public String submitMyForm() {
// Do stuff like validation and error marking in the model
return "my-form-view"; // Same as GET
}
}
The form GET and POST works just fine when written like this. You'll notice that the String arrays used for the #RequestMapping values are identical. What I want to do is put those URLs into one spot (ideally a static final field in the controller) so that when we add new URLs (which correspond to the form in future localized sites), we can just add them in one spot. So I tried this modification to the controller:
#Controller
public class MyFormController {
// Moved URLs up here, with references in #RequestMappings
private static final String[] MY_URLS =
{"/us-form.html", "/de-form.html", "/fr-form.html"};
// GET request
#RequestMapping(value=MY_URLS, // <-- considered non-constant
method={RequestMethod.GET})
public String showMyForm() {
// Do some stuff like adding values to the model
return "my-form-view";
}
// POST request
#RequestMapping(value=MY_URLS, // <-- considered non-constant
method={RequestMethod.POST})
public String submitMyForm() {
// Do stuff like validation and error marking in the model
return "my-form-view"; // Same as GET
}
}
The problem here is that the compiler complains about the value attribute no longer being a constant. I am aware that Spring requires that value must be a constant, but I had thought that using a final field (or static final in my case) with an Array literal containing String literals would have passed as "constant". My suspicion here is that the array literal has to be constructed on the fly in such a way that it is uninitialized when the value attribute is parsed.
I feel like this shouldn't be a hard thing to figure out with a basic Java knowledge, but something is escaping me that I haven't been able to find any answers for after some research. Can someone confirm my suspicion and give a citation or good explanation for why that may be so, or deny my suspicion and explain what the actual issue is?
Note: I cannot simply combine the URLs into a Path Pattern, as each form URL is in its localized site's language, and matching on that would be impossible. I merely give the "/{locale}-form.html" strings above as my URLs for example's sake.
You're right, this is nothing to do with Spring, all Annotation parameters must be compile-time constants. That's a basic java language rule.
Marking the array reference as final doesn't cut it because this is still perfectly legal:
MY_URLS[0] = "es-form.html";
Also, how locked in are you into embedding locale into the url like that in the first place? Are you emulating legacy links? Spring has plenty of built in support for using the browser's actual locale.
Related
I'm inspecting some code in a JAX-RS springboot microservice that I'm starting to work on. I saw the following (modified):
#POST
#Path("{foo: ([^/]+?)?}{bar: (/[^/]+?)?}")
public Response doit(
#PathParam("foo") String foo,
#PathParam("bar") String bar,
#RequestBody UpdateRequest updateRequest, #Context HttpHeaders httpHeaders);
That #Path value looks odd. Instead of having explicit "/" markers in the string, it's trying to do it through the regex. I'm guessing this can work, because this is existing code, but is this really advisable? Is there any reason that this would be necessary?
I suppose a similar questionable example would be this:
#Path("foo{bar: (/[^/]+?)?}")
Is there any reason this is better than the simpler:
#Path("foo/{bar}")
The JAX-RS specification, specifically the “URI Templates” section under the Resources chapter, has the answer:
Template parameters can optionally specify the regular expression used to match their values. The default value matches any text and terminates at the end of a path segment but other values can be used to alter this behavior, e.g.:
#Path("widgets/{path:.+}")
public class Widget {
...
}
In the above example the Widget resource class will be matched for any request whose path starts with widgets and contains at least one more path segment; the value of the path parameter will be the request path following widgets. E.g. given the request path widgets/small/a the value of path would be small/a.
So, if you don’t provide a customn regex, the default boundary is the /.
Therefore, that complex regex is unnecessary. #Path("{foo}/{bar}" is fine.
Technically, it’s not exactly the same; the regex forces {bar} to include the leading /. Is it worth the complexity of regexes that need extra visual analysis? Not in my opinion.
If you just used
#Path("foo/{bar}")
then calling /foo would lead to a 404, because the / is static and it would require requesting /foo/. But when it's in the regex of bar, it makes it optional. So the example
#Path("foo{bar: (/[^/]+?)?}")
allows you to access the parent resource and the sub resource from the same resource method. As a more realistic example say you have
#Path("customers{id: (/[^/]+?)?}")
With this, we would have a resource method that could handle both accessing a collection resource and an single resource. As opposed to having two separate resource methods, one for each case. For example
#GET
#Path("customers{id: (/[^/]+?)?}")
public Response get(#PathParam("id") String id) {
if (id == null) {
return collection customers collection
} else {
fetch custom by id and return customer.
}
}
That's the only real benefit I can see in this situation. Would probably need more context, maybe some documenting comments from the author as to what they were trying to accomplish. Overall though, IMO the code looks unnecessarily over complicated.
I'm going to implement a RESTful webservice using Spring.
Let it be an ordinary PUT method, something like this:
#RequestMapping(method=RequestMethod.PUT, value="/foo")
public #ResponseBody void updateFoo(#RequestBody Foo foo) {
fooService.update(foo);
}
In such a case input JSON format (if it corresponds to Foo class) will be successfully converted to Foo instance with no extra efforts, or error will be issued in case of wrong format.
But I'd like to make the service able to consume two different types of formats using same method (e.g. PUT) and same URL (e.g. /foo).
So that it possibly looked like:
//PUT method #1
#RequestMapping(method=RequestMethod.PUT, value="/foo")
public #ResponseBody void updateFoo(#RequestBody Foo foo) {
fooService.update(foo);
}
//PUT method #2
#RequestMapping(method=RequestMethod.PUT, value="/foo")
public #ResponseBody void updateFoo(#RequestBody FooExtra fooExtra) {
fooService.update(fooExtra);
}
and Spring converter tried to convert input JSON not only in Foo but in FooExtra as well and invoked corresponding PUT method depending on input format.
In fact, I tried to implement it exactly as it described above but without success. Is it even possible? Maybe, I need some kind of "trick"?
What is the best (and the most proper) way to achieve such behavior? Of course, I could always make two different URLs but I'd like to know whether it is possible with the same one.
Your attempt didn't work simply because Spring tried to match your methods against the request, by looking at url and method type, which are in both cases the same. It does not work like overloading in Java; argument types do not differentiate your methods.
But there are good news. SpringMVC can also examine request headers and request parameters when trying to match your handler methods. Since what you want to pass is actually pure metadata -an alternative format type of the same information- it makes perfect sense to use a custom request header. It's very easy to add custom headers when using a rest api. See the following link for JAX-RS: Adding a custom header.
Now in your server side you should configure the handler methods as:
//PUT method #1
#RequestMapping(method=RequestMethod.PUT, value="/foo", headers="returnType=Foo")
public #ResponseBody Foo updateFoo(#RequestBody Foo foo) {
fooService.update(foo);
}
//PUT method #2
#RequestMapping(method=RequestMethod.PUT, value="/foo", headers="returnType=FooExtra")
public #ResponseBody FooExtra updateFoo(#RequestBody FooExtra fooExtra) {
fooService.update(fooExtra);
}
Note also that if you want to access a return value with #ResponseBody you have to return your object, otherwise make the methods void
For understanding it we should think how Spring works, it uses Dispatcher Servlet. I don't think that spring does "combine" work for different types of input.
So my answer will be: "trick" with two different urls ;)
Given a Google Cloud Endpoints project in Eclipse with the servlet-class annotated with #Api(name="helloworld"), the Endpoints framework generates a file named war/WEB-INF/helloworld-v1.api when the project compiles successfully. Sometimes this file is not generated even if there are no compilation errors though - only what I will call "GAE Endpoints code convention errors".
Example - working:
public class TestEntity {
public String Text;
public TestEntity(String text){
Text = text;
}
}
#ApiMethod
public TestEntity getTestEntity(){
return new TestEntity("Hello world");
}
Example - NOT working:
// The TestEntity-class is unchanged
#ApiMethod
public TestEntity getTestEntity(String input){
return new TestEntity("Hello world");
}
The problem with the latter example is that I take a String parameter as input without annotating it with #Named. I know that in this example, but there might be other cases where this is not so obvious.
Is there anywhere where I can read some sort of error log on why the .api file is not generated?
Although I am a fan of code by convention, it really takes the programming efficiency a step back if I cannot get feedback on what I do wrong. Eclipse provides compiler error feedback. The Google Cloud Endpoints Framework should provide Code-By-Convention-Rule-Breaking feedback.
There isn't currently good logging or error messaging when code generation fails, though it's one of the (if not most) requested features. In the interim, here's a list of the common failure cases:
The return type is invalid. Return types must be objects conforming to JavaBean conventions, and types like Object, String, and Integer are not allowed.
One or more argument types are invalid. Methods may accept at most one object in the POST body, and this object should also conform to JavaBean conventions. Methods may accept zero or more arguments via the query string (using the #Named annotation) and these must be scalar types (e.g. String, Integer).
An API, method, or parameter has an invalid name. APIs, methods, and parameters should be named to match the following regular expression: [a-z]+[A-Za-z0-9]*. Convention also suggests using lowerCamelCase for naming (though alllowercase is allowed).
I'm working on converting a legacy project to Spring (trying to adjust little as possible for now) and I'm running into a small issue with mapping/translating legacy parameters to a model attribute object. I may be completely wrong in thinking about this problem but it appears to me that to translate a parameter to a specific model attribute setter is to pass in the request parameter through a method for creating a model attribute and manually call the correct setter:
#ModelAttribute("form")
public MyForm createMyForm(#RequestParameter("legacy-param") legacy) {
MyForm myForm = new MyForm();
myForm.setNewParam(legacy);
return myForm;
}
I don't necessarily want to change the request parameter name yet since some javascript and JSPs are depending on it being named that way but is there any way to do something like this? Or is there a different way to map/translate request parameters to model attributes?
public class MyForm {
#ParameterName("legacy-param")
private String newParam;
public void setNewParam(String value) { ... }
public String getNewParam() { ... }
}
#Controller
public class MyController {
#RequestMapping("/a/url")
public String myMethod(#ModelAttribute("form") MyForm myForm, BindingResult result) { ... }
}
The way you've written that model attribute method is indeed odd. I'm not entirely clear what you're actually trying to do.Assuming there are many parameters, you're going to end up with an awful lot of instances of MyForm in your ModelMap. A more 'normal' way to create model attribute would be like this:
#ModelAttribute("legacyParamNotCamel")
public MyForm createMyForm(#RequestParameter("legacy-param-not-camel") String legacy) {
return legacy;
}
Then in the JSP you can refer to it directly in expression language. e.g.,
<c:out value="${legacyParamNotCamel}"/>
If you want to put them onto a form backing object, you need to do it all in a single method that creates the object, not make new copies of it in each method. (assuming your form has more than a single parameter associated with it.)
--
It seems like what you're really trying to do though is translate the parameter names in the request before the web data binder gets ahold of it, so that you can bind oddly named parameters onto a java bean? For that you'll need to use an interceptor that translates the names before the binding process begins, or make your own subclass of the databinder than can take a property name translation map.
You placed the #ModelAttribute at the Method Level but the intention seems to be more of a formBackingObject hence we should be dealing at the Method Parameter Level
There's a difference.
I put up an explanation here on my blog along examples at Spring 3 MVC: Using #ModelAttribute in Your JSPs at http://krams915.blogspot.com/2010/12/spring-3-mvc-using-modelattribute-in.html
is it possible to configure GET method to read variable number of URI parameters and interpret them either as variable argument (array) or collection? I know query parameters can be read as list/set but I can't go for them in my case.
E.g.:
#GET
#Produces("text/xml")
#Path("list/{taskId}")
public String getTaskCheckLists(#PathParam("taskId") int... taskId) {
return Arrays.toString(taskId);
}
Thanks in advance
If I understand your question correctly, the #Path annotation can take a regular expression to specify a list of path components. For example, something like:
#GET
#Path("/list/{taskid:.+}")
public String getTaskCheckLists(#PathParam("taskid") List<PathSegment> taskIdList) {
......
}
There's a more extensive example here.
I am not submitting this as an answer as it is merely an edge case on the currently accepted answer which is what I've also used.
In my case (Jersey 1.19) /list/{taskid:.+} would not work for the edge case of zero variable parameters. Changing the RegEx to /list/{taskid:.*} took care of that. See also this article (which seems to be applicable).
Moreover, upon changing the regexp to cardinality indicator to * (instead of +) I also had to deal programmatically with the case of empty strings as I would translate the List<PathSegment> into a List<String> (to pass it into my DB-access code).
The reason I am translating from PathSegment to String is that I didn't want a class from the javax.ws.rs.core package to pollute my Data Access Layer code.
Here's a complete example:
#Path("/listDirs/{dirs:.*}")
#GET
#Produces(MediaType.APPLICATION_JSON)
public Response listDirs(#PathParam("dirs") List<PathSegment> pathSegments) {
List<String> dirs = new ArrayList<>();
for (PathSegment pathSegment: pathSegments) {
String path = pathSegment.getPath();
if ((path!=null) && (!path.trim().equals("")))
dirs.add(pathSegment.getPath());
}
List<String> valueFromDB = db.doSomeQuery(dirs);
// construct JSON response object ...
}