I'm trying to build a RESTful API using Spring MVC. I'm shooting for clean and manageable code where the package structure follows the url structure.
So here is what I've got:
// com.test.api.library
#RequestMapping("/library/{libraryId}")
public Library getLibrary(#PathVariable long libraryId) {
return service.getLibraryById(libraryId);
}
// com.test.api.library.book
#RequestMapping("/library/{libraryId}/book/{bookId}")
public Book getBook(#PathVariable long libraryId, #PathVariable long bookId) {
Library library service.getLibraryById(libraryId);
return library.getBookById(bookId);
}
While this works, I find it messy and error-prone to have to repeat "/library/{libraryId}" in all inherited #RequestMappings, /library is likely to be to root of a big part of the API and it should be written once and reused instead of written everywhere.
I would like to rewrite the book-class to something like this:
// com.test.api.library.book
#RequestMapping("/book/{bookId}")
public Book getBook(#PathVariable long bookId) {
// long libraryId magically given to me from the library-class's getLibrary()
Library library service.getLibraryById(libraryId);
return library.getBookById(bookId);
}
Is there any way Spring can help me here? It is acceptable for me to use normal java inheritance, spring annotation or anything else that helps me to not write "/library/{libraryId}" as a part of every url I ever write.
I believe this question has been asked & answered before: Spring MVC #RequestMapping Inheritance
That said, here is one way to reduce the amount of duplicate information. I don't actually do this in my own code because I think having the URI right next to the code is more maintainable, even if it means a little duplication.
#RequestMapping(URI_LIBRARY)
public interface LibraryNamespace {
public static String URI_LIBRARY = "/library/{libraryId}";
}
#RequestMapping(URI_BOOK)
public interface BookNamespace {
public static String URI_BOOK = LibraryNamespace.URI_LIBRARY + "/book/{bookId}";
}
#Controller
public class LibraryController implements LibraryNamespace {
#RequestMapping("")
public Library get(#PathVariable long libraryId) {
return service.getLibraryById(libraryId);
}
}
#Controller
public class BookController implements BookNamespace {
#RequestMapping("")
public Book get(#PathVariable long libraryId, #PathVariable long bookId) {
Library library service.getLibraryById(libraryId);
return library.getBookById(bookId);
}
}
Since I wouldn't take this approach myself, I haven't actually tried this solution! Based on my understanding of Spring, I think it should work though...
Use a polymorphic parent approach.
#Controller
public class CommentsController {
#RequestMapping(value="/comments", method = RequestMethod.GET)
public #ResponseBody String index() {
/* kludge to allow optional path parameters */
return index(null, null);
}
#RequestMapping(value="/{parent_collection}/{parent_id}/comments", method = RequestMethod.GET)
public #ResponseBody String index(#PathVariable("parent_collection") String parentCollection, #PathVariable("parent_id") String parentId) {
if (parentCollection == null) {
return "all comments";
}
else if ((parentCollection != null) && (parentCollection.equals("posts"))) {
/* get parent, then get comments for parent */
return "comments for single post";
}
else if ((parentCollection != null) && (parentCollection.equals("customers"))) {
/* get parent, then get comments for parent */
return "comments for single customer";
}
else if ((parentCollection != null) && (parentCollection.equals("movies"))) {
/* get parent, then get comments for parent */
return "comments for single movie";
}
}
#RequestMapping(value = "/comments/{id}", method = RequestMethod.GET)
public #ResponseBody String show(#PathVariable Integer id) {
/* kludge to allow optional path parameters */
return show(null, null, id);
}
#RequestMapping(value = "/{parent_collection}/{parent_id}/comments/{id}", method = RequestMethod.GET)
public #ResponseBody String show(#PathVariable("parent_collection") String parentCollection, #PathVariable("parent_id") String parentId, #PathVariable Integer id) {
/* get comment, then get parent from foreign key */
if (parentCollection == null) {
return "single comment";
}
else if ((parentCollection != null) && (parentCollection.equals("posts"))) {
return "single comment for single post";
}
else if ((parentCollection != null) && (parentCollection.equals("customers"))) {
return "single comment for single customer";
}
else if ((parentCollection != null) && (parentCollection.equals("movies"))) {
return "single comment for single movie";
}
}
}
Additionally, you could use a base controller to route the URI prefix to parent resources (/libraries/{library_id}/../..), add the parent models to the request scope, and then let the regular request mappings handle the rest of the URI to child resources (/../../books/1). I don't have an example of this off-hand.
Side note. Singular nested resources are generally regarded as an antipattern for URI design. A controller should handle its own resources. The most common implementations make the key for the singular nested resource unique, i.e., not dependent on its parent resource. For instance, a database record primary key. However, there are situations where the key might not be unique, such as an ordinal or position value (e.g., book 1, chapter 1, chapter 2), or maybe even a natural key (e.g., book ISBN, person SSN, email address, username, filename).
Example of canonical URIs for nested resources:
/articles => ArticlesController#index
/articles/1 => ArticlesController#show
/articles/1/comments => CommentsController#index
/articles/1/comments/2 => CommentsController#show (okay, but not preferred)
/comments/2 => CommentsController#show (preferred)
I don't think it's possible. But you can have the #RequestMapping annotation on the class itself, so it will save you at least some typing.
#Controller
#RequestMapping("/library/{libraryId}")
public class HelloWorldController {
#RequestMapping(value="/book/{bookId}")
public ModelAndView helloWorld() {
....
}
}
Related
I am starting to learn the Spring boot framework, especially for developing an application under the MVC concept with Spring Boot. I found an article and tried to re-implement:
https://www.toptal.com/spring/beginners-guide-to-mvc-with-spring-framework
I used Java 8, Spring Boot 2.5.1, and Gradle. During the implementation, I got several errors especially in this section:
#RequestMapping(value="/developer/{id}/skills", method=RequestMethod.POST)
public String developersAddSkill(#PathVariable Long id, #RequestParam Long skillId, Model model) {
Skill skill = skillRepository.findOne(skillId);
Developer developer = repository.findOne(id);
if (developer != null) {
if (!developer.hasSkill(skill)) {
developer.getSkills().add(skill);
}
repository.save(developer);
model.addAttribute("developer", repository.findOne(id));
model.addAttribute("skills", skillRepository.findAll());
return "redirect:/developer/" + developer.getId();
}
model.addAttribute("developers", repository.findAll());
return "redirect:/developers";
}
I am a new learner of Spring Boot and Java. The problem like similar to the previous post.
I modified the code by following the Eclipse recommendation and discussion in the previous post.
This is my new code:
#RequestMapping(value="/developer/{id}/skills", method=RequestMethod.POST)
public String developersAddSkill(#PathVariable Long id, #RequestParam Long skillId, Model model) {
Optional<Skill> skill = skillRepository.findById(skillId);
Optional<Developer> optionalDeveloper = repository.findById(id);
Developer developer = optionalDeveloper.get();
if (developer != null) {
if (!developer.hasSkill(skill)) {
developer.getSkills().add(skill.get());
}
repository.save(developer);
model.addAttribute("developer", repository.findById(id));
model.addAttribute("skills", skillRepository.findAll());
return "redirect:/developer/" + developer.getId();
}
model.addAttribute("developers", repository.findAll());
return "redirect:/developers";
}
However, there is still one error:
The method hasSkill(Skill) in the type Developer is not applicable for the arguments (Optional<Skill>
I tried to fix it, but I am still cannot do it.
How can I fix this problem? Could everyone suggest the solution? I would appreciate it.
Thank you.
You have two problems:
You are doing
Developer developer = optionalDeveloper.get();
if (developer != null) {
...
without checking if the Optional<Developer> is present.
If the Optional is not present (represents the absence of a value),
then this will throw a NoSuchElementException - it will not return null.
So, this should be:
if(optionalDeveloper.isPresent()) {
...
You have
if (!developer.hasSkill(skill)) {
developer.getSkills().add(skill);
}
but developer.hasSkill() takes a Skill as an argument - you are passing an Optional.
So, this should be:
if(optionalSkill.isPresent()) {
final Skill skill = optionalSkill.get());
if (!developer.hasSkill(skill)) {
developer.getSkills().add(skill);
}
}
Its simple. You are passing Optional varibale inside the method when it actually expects skill object.
Modified code:
#RequestMapping(value="/developer/{id}/skills", method=RequestMethod.POST)
public String developersAddSkill(#PathVariable Long id, #RequestParam Long skillId, Model model) {
Optional<Skill> optionalSkill = skillRepository.findById(skillId);
Optional<Developer> optionalDeveloper = repository.findById(id);
if (optionalDeveloper.isPresent() && optionalSkill.isPresent()) {
Developer developer = optionalDeveloper.get();
Skill skill = optionalSkill.get();
if (!developer.hasSkill(skill)) {
developer.getSkills().add(skill);
}
repository.save(developer);
model.addAttribute("developer", developer);
model.addAttribute("skills", skillRepository.findAll());
return "redirect:/developer/" + developer.getId();
}
model.addAttribute("developers", repository.findAll());
return "redirect:/developers";
}
I need to fetch data from API. Right now I have controller and service:
#RestController
public class SearcherController {
private SearcherService searcherService;
public SearcherController(SearcherService searcherService) {
this.searcherService = searcherService;
}
#GetMapping("/{name}")
public ResponseEntity<Output> getUrl(#RequestParam(value = "name", required = false) String name) {
if (name == null) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new Output("BAD REQUEST"));
} else {
return ResponseEntity.status(HttpStatus.OK).body(searcherService.getOutput(name));
}
}
}
In the controller, I check if my request param is empty. If so I return bad request.
If it is not null I go to the service. And in service, I do the call to API.
#Service
public class SearcherService {
public Output getName(String name) {
List<Result> results = getDataFromApi(name);
// call to Api and other calculation
}
}
And fetching data from API could give me a list with results or could give me an empty list.
And that is my question.
What is the best practice? Should I move this code to controller call the API in the controller and there make results !=null and do something about it? I'm thinking about best practice in this case...
Stack is Spring Boot w/ Jetty/Jersey. Here's the resource method in question:
#GET
#Path("campaignTargets")
#Produces(MediaType.APPLICATION_JSON)
#Transactional(readOnly=true)
public List<CampaignTargetOutputDTO> getCampaignTargets(
#PathParam("businessUnitId") Integer id,
#QueryParam("name") String name,
#Pattern(regexp = DATE_VALIDATION_PATTERN) #QueryParam("startDate") String startDate,
#Pattern(regexp = DATE_VALIDATION_PATTERN) #QueryParam("endDate") String endDate,
#Pattern(regexp = INTEGER_CSV_VALIDATION_PATTERN) #QueryParam("targetTypeIds") String targetTypeIds,
#Pattern(regexp = ALPHANUM_CSV_VALIDATION_PATTERN) #QueryParam("statuses") String statuses) {
return ResourceUtil.entityOr404(campaignService.getAdvertiserCampaignTargets(id, name, startDate, endDate, targetTypeIds, statuses));
}
When Jersey intercepts the call to this method to perform the validation, it doesn't (always) get this method. The reason I know this is because I have taken the advice of the Jersey documentation and created the following ValidationConfig:
#Provider
public class ValidationConfigurationContextResolver implements
ContextResolver<ValidationConfig> {
#Context
private ResourceContext resourceContext;
#Override
public ValidationConfig getContext(Class<?> type) {
final ValidationConfig config = new ValidationConfig();
config.constraintValidatorFactory(
resourceContext.getResource(InjectingConstraintValidatorFactory.class));
config.parameterNameProvider(new CustomParameterNameProvider());
return config;
}
private static class CustomParameterNameProvider extends DefaultParameterNameProvider {
private static final Pattern PROXY_CLASS_PATTERN = Pattern.compile("(.*?)\\$\\$EnhancerBySpringCGLIB\\$\\$.*$");
public CustomParameterNameProvider() {
}
#Override
public List<String> getParameterNames(Method method) {
/*
* Since we don't have a full object here, there's no good way to tell if the method we are receiving
* is from a proxy or the resource object itself. Proxy objects have a class containing the string
* $$EnhancerBySpringCGLIB$$ followed by some random digits. These proxies don't have the same annotations
* on their method params as their targets, so they can actually interfere with this parameter naming.
*/
String className = method.getDeclaringClass().getName();
Matcher m = PROXY_CLASS_PATTERN.matcher(className);
if(m.matches()) {
try {
return getParameterNames(method.getDeclaringClass().getSuperclass().
getMethod(method.getName(), method.getParameterTypes()));
} catch (Exception e) {
return super.getParameterNames(method);
}
}
Annotation[][] annotationsByParam = method.getParameterAnnotations();
List<String> paramNames = new ArrayList<>(annotationsByParam.length);
for(Annotation[] annotations : annotationsByParam) {
String name = getParamName(annotations);
if(name == null) {
name = "arg" + (paramNames.size() + 1);
}
paramNames.add(name);
}
return paramNames;
}
private String getParamName(Annotation[] annotations) {
for(Annotation annotation : annotations) {
if(annotation.annotationType() == QueryParam.class) {
return ((QueryParam) annotation).value();
} else if(annotation.annotationType() == PathParam.class) {
return ((PathParam) annotation).value();
}
}
return null;
}
}
}
My main problem with this solution is that it requires a paragraph of comment to (hopefully) prevent future confusion. Otherwise it seems to work. Without this, I get uninformative parameter names like arg1 and so on, which I'd like to avoid. Another big problem with this solution is that it relies too heavily on the implementation of Aop proxying in Spring. The pattern may change and break this code at some point in the future and I may not be here to explain this code when the comment fails to illuminate its purpose. The weirdest thing about this is that it seems to be intermittent. Sometimes the parameter names are good and sometimes they're not. Any advice is appreciated.
It turns out this happens as a result of running the server from eclipse. I haven't quite figured out why, but running the server from the command line fixes the problem. If anyone can figure out why eclipse does this and how to turn off whatever "feature" of eclipse is causing this, I will upvote/accept your answer. For now the answer is, don't run the service in eclipse.
This is the following code of my RESTful service class:
#RequestScoped
#Path("/empresas")
public class EmpresaEndpoint {
#Inject
private EmpresaRB empresaRB;
#GET
#Path("/{id:[0-9][0-9]*}")
#Produces("application/json")
public Response findById(#PathParam("id") final Long id) {
//TODO: retrieve the empresas
Empresa empresas = null;
if (empresas == null) {
return Response.status(Status.NOT_FOUND).build();
}
return Response.ok(empresas).build();
}
#GET
#Produces("application/json")
public List<Empresa> listAll(
#QueryParam("start") final Integer startPosition,
#QueryParam("max") final Integer maxResult) {
//TODO: retrieve the empresa
return empresaRB.getEmpresas();
}
}
If I wanted to access all the data stored on "Empresa" via jQuery, I would do:
$.getJSON( "rest/empresas", function( data ) {
//whatever is needed.
}
The code above would access the "listAll" method. So how can I access the "findById" method and pass the necessary parameter?
Assuming you have a variable called empresaId that holds the id for the entity, this should work.
$.getJSON( "rest/empresas/" + empresaId, function(data) {
// Whatever is required here
}
Well without having used that particular framework, it looks like it's mapping to the right method based on the path - it will use findById if the path has an ID, e.g.
$.getJSON("rest/empresas/100", function(data) {
// ...
}
(That will find the item with ID 100... obviously substitute the ID of the item you want to find. We don't know where that's coming from, but "rest/empresas/" + id may well be all you need.)
In my initial code, there was no query being connected to the variable "empresa", on the method findById().
I created a query on the repository class and assigned it to the variable. Problem Solved.
Thank you all for the time lended.
help me please.
I have the code of controller like this:
#RequestMapping(method = RequestMethod.GET)
public String showOrders(#RequestParam(value = "status", required = false) String status, Model model) {
if(status != null) {
Order.Status orderStatus = Order.Status.valueOf(status);
if (orderStatus != null) model.addAttribute("currentStatus", orderStatus);
}
return "admin/orders";
}
#ModelAttribute("currentStatus")
public Order.Status populateCurrentStatus() {
return Order.Status.PAYMENT;
}
#ModelAttribute("orders")
public List<Order> populateOrders(#ModelAttribute("currentStatus") Order.Status status) {
return orderBo.getByStatus(status);
}
I want the default currentStatus to be equal to Order.Status.PAYMENT, but if the controller receives a GET request with the argument status (on method showOrders), then replace currentStatus in the model to the status transmitted in the request.
And the populateOrders should return a different list of orders, in accordance with the new status. But unfortunately, this is not happening. Method populateOrders always gets currentStatus equal Order.Status.PAYMENT and it never changes.
You can add #RequestParam to your #ModelAttribute definition in order to populate #ModelAttribute differently, depending on a request parameter.
This should work:
#RequestMapping(method = RequestMethod.GET)
public String showOrders() {
//no need to manually modify currentStatus in the model anymore
return "admin/orders";
}
#ModelAttribute("currentStatus")
public Order.Status populateCurrentStatus(#RequestParam(value = "status", defaultValue = "PAYMENT") String status) {
return Order.Status.valueOf(status);
}
#ModelAttribute("orders")
public List<Order> populateOrders(#ModelAttribute("currentStatus") Order.Status status) {
return orderBo.getByStatus(status);
}
Personal note:
I personally dislike Spring's #ModelAttribute system for defining default model attributes, especially in case of more complex models or in more complex controllers. It becomes hard to maintain because it's hard to control what exactly goes into the model (for example, for some requests, maybe you don't want the whole model populated, e.g. you only need currentStatus but not the list of orders. I prefer populating the model manually (Model.addAttribute(...)) - it's not much extra work and everything is explicit and easy to debug if a problem pops out later on.