16

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.

Maksim Solovjov
  • 3,147
  • 18
  • 28
Andreas Wederbrand
  • 38,065
  • 11
  • 68
  • 78

4 Answers4

4

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...

Community
  • 1
  • 1
jtoberon
  • 8,706
  • 1
  • 35
  • 48
  • I'm starting to think that the answer is "no, it is not possible". Your idea adds a level of abstraction but still doesn't solve the main issue which is handling the "@PathVariable long libraryId" somewhere closer to the library-class. Coming from "normal" java I'm used to inheritance and letting the parent class handle it's own variables and just letting the subclasses handle what is specific to them. Thanks anyway. – Andreas Wederbrand Sep 12 '11 at 06:50
  • It's not possible. Sorry I didn't make this more clear here. I thought the question/answer that I linked to was clear enough. – jtoberon Sep 12 '11 at 12:53
  • I have done this. If you combine this approach with the polymorphic parent approach, then you can get DRY paths and separation-of-concern arguments. I'll dig up my code and post an answer. – Alex Mar 07 '13 at 05:48
  • I agree with you. Having the URI right next to the code is more maintainable and readable. – Michel Sep 12 '14 at 08:31
3

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)
Alex
  • 5,909
  • 2
  • 35
  • 25
1

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.

Bozho
  • 588,226
  • 146
  • 1,060
  • 1,140
  • Yeah, I'm aware of that possibility and it's way better than just annotating the the methods but still a long way from perfect. – Andreas Wederbrand Aug 29 '11 at 19:47
  • Based on your response that its a long way from perfect its unclear to me what you want. Can you maybe show the full urls of a couple resources, and describe which controller(s) you want them handled by? – SingleShot Sep 08 '11 at 20:58
  • Right, this might be a weird example but... `/country/{countryCode}/state/{stateCode}/city/{cityCode}/street/{streetCode}/number/{streetNumber}` each of these steps (ie, country, state, city, street and number) should go into it's own Controller and each have a subset of verbs on them. The main question is when I do the numberController that I don't won't to repeat the url all the way from country and I don't wan't to "know" that countryCode is the key to this resource. I just want to "have" it. – Andreas Wederbrand Sep 09 '11 at 06:07
  • This is a bit off-target, but I implemented a system sort-of like this by mapping command/verb pairs (ultimately the same as entity/id pairs--the command is "lookup") to beans stored in a DB, implemented by Groovy. Each result type had a list of available commands (so a country had state available). It got pretty complicated because of my use-case, but was flexible, and extensible at run-time. Maybe it'll give you some ideas, although I know it doesn't meet your immediate need. – Dave Newton Sep 10 '11 at 18:25
  • I'm awarding this answer with the bounty. I don't want to waste the bounty on nothing and it was the "right" answer in a way. – Andreas Wederbrand Sep 14 '11 at 09:20
-1
@Controller
@RequestMapping("/library/{libraryId}")
public class HelloWorldController {

    @RequestMapping(value="/book/{bookId}")
    public ModelAndView helloWorld() {
        ....
    }

}
Anthony
  • 906
  • 1
  • 8
  • 19
  • That doesn't answer my question. I'm well aware of this possibility (if nothing else it was pointed out in the answer given by Bozho). Doing like this means I have to put all subresourses to Library in the same Controller. That is possible ofcourse but not what I'm asking for. Thanks anyway. – Andreas Wederbrand Sep 09 '11 at 06:10