Use multiple child Controllers against #RequestMapping defined in Parent class - java

I have a parent class P which defines one request mapping like this:
public abstract class P {
#RequestMapping(value = "/a/b/c", method = RequestMethod.POST)
public String productLink(#RequestParam("abc") String json) throws Exception {
return getProductLinks(json);
}
}
and I have couple of children Controller classes and ClassImpl is one of them:
#Controller
public class ClassImpl extends P {
#RequestMapping(value = "/start", method = RequestMethod.GET)
public String start(#RequestParam(value = "keyword", required = true) String keyword,
#RequestParam(value = "keywordId", required = true) long keywordId) throws Exception {
//Something
}
}
If I run this app with only one child class, it works fine but it causes issues with multiple child controllers.
When I run my application, I get an error saying "Cannot map handler ClassImpl to URL path [/a/b/c]: There is already handler [a.b.c.d.ClassImpl#a92aaa] mapped"
It seems that because of multiple child classes, it is unable to find the controller for this mapping which is understood.
Is defining #RequestMapping in each class (or one separate class) the only way? I don't want to put similar code at all the places. Is there any workaround for this to keep it in parent class and keep using it?
Thanks,

Is defining #RequestMapping in each class (or one separate class) the only way?
The short answer is yes. Personally I think that it belongs in a separate class.
Why exactly do you want to put productLink() in the parent class, anyway? It's not an abstract method and you're not overriding it, so to me it doesn't make much sense.

You should not use #RequestMapping in abstract classe. This annotation is for real controller, so concrete classe.
Use Abstract class as they are intented to use, ie to factorize code, not to do the work.
Here you can do something like :
public abstract class P {
public String productLink(String json) throws Exception {
return getProductLinks(json);
}
}
and then
#Controller
public class ClassImpl extends P {
#RequestMapping(value = "/start", method = RequestMethod.GET)
public String start(#RequestParam(value = "keyword", required = true) String keyword,
#RequestParam(value = "keywordId", required = true) long keywordId) throws Exception {
//Something
}
//here reusing the code from superclass
#RequestMapping(value = "/a/b/c", method = RequestMethod.POST)
public String productLink(#RequestParam("abc") String json) throws Exception {
return super.getProductLinks(json);
}
}
This add a bit of boilerplate code, but this is the way to do it IMHO.

Related

How to split classes to use serveral #Controller

I am learning spring boot, and i developed the below simple example. I would like to annotate a class as Controller using #Controller. this class has constructor and I want to have access to GreetingFromDeuController as shown:
http://localhost:8080:/GreetingFromDeuController?str = "hi"
the error i am receiving is
#RequestMapping is not applicable on a constructor
please let me know how to solve.
code:
#Controller
#RequestMapping("/GreetingFromDeuController")
public class GreetingFromDeuController {
private String str;
#RequestMapping("/GreetingFrom/deu")
GreetingFromDeuController(#RequestParam(value = "str") String str) {
this.str = str;
}
#RequestMapping("/GreetingFromDeuController")
public String getGreetingFromDeu() {
return this.str;
}
}
First of all your constructor gets initialize much before you hit your URL. So you need to work on your design or tell me your business requirement and I will try to provide you a solution. My refactor code solution will help you to achieve that in two steps. First hit POST method which will do work on setting variable and then subsequent hits of GET method will return that set value.
We can refactor code like below. It will explain use of RequestMapping on method and class.
Considering we have to write two API, one for reading and one for writing.
URLS :
1. POST http://localhost:8080/example/greetings (in request body send {str:'hi'})
2. GET http://localhost:8080/example/greetings
#Controller
#RequestMapping("/example")
public class GreetingFromDeuController {
private String str;
#RequestMapping(value="/greetings" , method = RequestMethod.POST)
public void setGreetingFromDeu(#RequestBody(value = "str") String str)
{
this.str = str;
}
#RequestMapping(value="/greetings" , method = RequestMethod.GET)
public String getGreetingFromDeu()
{
return this.str;
}
}
The #RequestMapping documentation says:
Annotation for mapping web requests onto methods in request-handling
classes with flexible method signatures.
Then you can not do that, if you want to initialize your variables or whatever you can use several ways:
1.- Use #PostConstruct
#PostContruct
public void init() {
this.str = "Anything";
}
2.- Use a simple request to set anything only
#RequestMapping(value="/refresh/anythings", method = RequestMethod.PUT)
public void refresh(#RequestBody(value = "str") String str) {
this.str = str;
}
3.- Use #Value
In application.properties / application.yaml
properties.str = anything
In the Controller
#Value("${properties.str:default}") // by default str is "default"
public String str;
#RequestMapping(value="/greetings" , method = RequestMethod.GET)
public String getGreetingFromDeu() {
return this.str;
}
As far I am concerned, #RequestMapping is not meant for constructors. It should be used for annotating methods or classes. Methods that are responsible for handling requests.
#RequestMapping should be used to map request with endPoint. which can be used as class level and method level.
You can use #RestController (improved from #Controller see difference).
The ideal flow for Spring Boot is Controller -> Service -> Repository
Controller -> maps request with endPoint and return response
Service -> perform business logic
Repository -> Handle database operation
Example
#RestController
#RequestMapping("/api")
public class GreetingController {
#Autowired GreetinService greetingService;
// Request http://localhost:8080/api/GreetingFrom
#GetMapping("/GreetingFrom")
public ResponseEntity<String> GreetingRequestParam(#RequestParam(value = "name") String name) {
greetingService.performBusinessLogic(name);
return new ResponseEntity<String>("Greetings from "+name,HttpStatus.OK);
}
// Request http://localhost:8080/api/GreetingFrom/user2121
#GetMapping("/GreetingFrom/{name}")
public ResponseEntity<String> GreetingPathVariable(#PathVariable(value = "name") String name) {
return new ResponseEntity<String>("Greetings from "+name,HttpStatus.OK);
}
}

Is it possible to map a nested mapping in spring-mvc to a global path?

I have something like this:
#RestController
#RequestMapping("/{id}")
public class MyController {
#GetMapping
public String get(#PathVariable String id) {
...
}
#PostMapping
public String post(#PathVariable String id, Payload payload) {
...
}
#GetMapping("/deeper/{id}")
public String getDeeper(#PathVariable String id) {
....
}
}
This gives 3 mappings:
/{id} (GET)
/{id} (POST)
/{id}/deeper/{id} (GET)
I would like the third of them to be just /deeper/{id} (GET).
Is it possible to do this leaving the method in the same controller and leaving that controller-wise #RequestMapping annotation?
What you request is not possible because you cannot avoid a requestMapping on a class level which makes no sense because being on class level means that you want that path to affect to all your methods.
Keep in mind that a RestController is RESTful and a class level requestMapping is used to avoid adding the same resource path to every method, so it does not make sense to have a method that can't fit in within that resource (you should move it to another controller instead).
This being said, there are a few things you can try:
1 This is not recommended. Use more than one path value on your class #ResquestMapping, in your case:
#RestController
#RequestMapping("/{id}", "/")
public class MyController{...}
You can kinda achieve what you want with this but this is extremely
discouraged and a code smell because basically means that all your methods will accept url paths either starting with id or with /, think carefully if you want to use this approach.
2 The recommended one, Remove the #RequestMapping in the class level and just update the path on every method, in your case:
#RestController
public class MyController {
#GetMapping(value = /{id})
public String get(#PathVariable String id) {
...
}
#PostMapping(value = "/{id}")
public String post(#PathVariable String id, Payload payload) {
...
}
#GetMapping("/deeper/{id}")
public String getDeeper(#PathVariable String id) {
....
}
}
3 also a recommended one Just move the method that does not fit in your controller "general logic" to another controller class which make's sense since that method is not affected by the controller general logic.

spring two method with same URL mapping

I have 2 class with 2 same URL mappings:
#RequestMapping(value = "/topics/{id}", method = RequestMethod.GET)
public ModelAndView methodA(#PathVariable(TOPIC_ID) Long topicId) {
...
}
//
#RequestMapping(value = "/topics/{id}", method = RequestMethod.GET)
public ModelAndView methodB(#PathVariable(TOPIC_ID) Long topicId) {
...
}
MethodB is in a class that is loaded dynamically. I want use methodA only if methodB is not available. If methodB is available the Spring uses only it.
How can I do that.
It sounds really confusing to sometimes have the URL mapping come from one package and sometimes from another. Why don't you do as Ken Bekov suggested in a comment and have just one class where there's the URL mapping and have that class dynamically decide which implementation to use? So something like this:
#RequestMapping(value = "/topics/{id}", method = RequestMethod.GET)
public ModelAndView methodA(#PathVariable(TOPIC_ID) Long topicId) {
Class classWithMapping;
try {
classWithMapping = Class.forName("class.that.MayNotExist");
} catch(ClassNotFoundException cnfe) {
classWithMapping = MyDefaultClass.class;
}
// ....
}
...and then instantiate classWithMapping using reflection or Spring's application context.
The spring way would be to have only one controller mapped to an URL, and to inject the dynamic class that actually does the job in it:
class A {
#Autowired(required = false)
class B b; // inject by Spring or by ... or not at all
...
#RequestMapping(value = "/topics/{id}", method = RequestMethod.GET)
public ModelAndView methodA(#PathVariable(TOPIC_ID) Long topicId) {
if (b != NULL) { // class B has been found and injected
return b.methodB(topicId)
}
// fallback ...
...
}
}
class B {
// NO #RequestMapping here !
public ModelAndView methodB(#PathVariable(TOPIC_ID) Long topicId) {
...
}
}
In spring, a controller object is a singleton bean in spring context, and the context is initialized during starting. So, if a class is dynamically loaded, the request mapping will not be applied. So you can not do what you said.
The solution above of ZeroOne is the only way I think.

Spring Data Rest / Spring Hateoas Custom Controller - PersistentEntityResourceAssembler

I'm attempting to add some additional business logic to the auto-generated endpoints from the RepositoryRestResource. Please see the code below:
Resource:
#RepositoryRestResource(collectionResourceRel="event", path="event")
public interface EventRepository extends PagingAndSortingRepository<Event, Long> {
}
Controller:
#RepositoryRestController
#RequestMapping(value = "/event")
public class EventController {
#Autowired
private EventRepository eventRepository;
#Autowired
private PagedResourcesAssembler<Event> pagedResourcesAssembler;
#RequestMapping(method = RequestMethod.GET, value = "")
#ResponseBody
public PagedResources<PersistentEntityResource> getEvents(Pageable pageable,
PersistentEntityResourceAssembler persistentEntityResourceAssembler) {
Page<Event> events = eventRepository.findAll(pageable);
return pagedResourcesAssembler.toResource(events, persistentEntityResourceAssembler);
}
}
I've looked at the following two stackoverflow articles:
Can I make a custom controller mirror the formatting of Spring-Data-Rest / Spring-Hateoas generated classes?
Enable HAL serialization in Spring Boot for custom controller method
I feel like I am close, but the problem that I am facing is that:
return pagedResourcesAssembler.toResource(events, persistentEntityResourceAssembler);
returns an error saying:
"The method toResource(Page<Event>, Link) in the type PagedResourcesAssembler<Event> is not applicable
for the arguments (Page<Event>, PersistentEntityResourceAssembler)".
The toResource method has a method signature that accepts a ResourceAssembler, but I'm not sure how to properly implement this and I can't find any documentation on the matter.
Thanks in advance,
- Brian
Edit
My issue was that I thought I could override the controller methods that are auto-created from #RepositoryRestResource annotation without having to create my own resource and resource assembler. After creating the resource and resource assembler I was able to add my business logic to the endpoint.
Resource:
public class EventResource extends ResourceSupport {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Resource Assembler:
#Component
public class EventResourceAssembler extends ResourceAssemblerSupport<Event, EventResource> {
public EventResourceAssembler() {
super(EventController.class, EventResource.class);
}
#Override
public EventResource toResource(Event entity) {
EventResource eventResource = createResourceWithId(entity.getId(), entity);
eventResource.setName(entity.getName());
return eventResource;
}
}
Updated Controller:
#RepositoryRestController
#RequestMapping(value = "/event")
public class EventController {
#Autowired
private EventRepository eventRepository;
#Autowired
private EventResourceAssembler eventResourceAssembler;
#Autowired
private PagedResourcesAssembler<Event> pageAssembler;
#RequestMapping(method = RequestMethod.GET, value = "")
#ResponseBody
public PagedResources<EventResource> getEvents(Pageable pageable) {
Page<Event> events = eventRepository.findAll(pageable);
// business logic
return pageAssembler.toResource(events, eventResourceAssembler);
}
}
The thing I don't like about this is that it seems to defeat the purpose of having a RepositoryRestResource. The other approach would be to use event handlers that would get called before and/or after the create, save, delete operations.
#RepositoryEventHandler(Event.class)
public class EventRepositoryEventHandler {
#HandleBeforeCreate
private void handleEventCreate(Event event) {
System.out.println("1");
}
}
There doesn't seem to be any events for the findAll or findOne operations. Anyways, both these approaches seem to solve my problem of extending the auto generated controller methods from RepositoryRestResource.
It requires a PagedResourcesAssembler, Spring will inject one for you if you ask.
public PagedResources<Foo> get(Pageable page, PagedResourcesAssembler<Foo> assembler) {
// ...
}
In this case the resource is Foo. It seems in your case the resource you're trying to return is an Event. If that's so, I would expect your code to look something like:
private ResourceAssembler<Event> eventAssembler = ...;
public PagedResources<Event> get(Pageable page, PagedResourcesAssembler<Event> pageAssembler) {
Event event = ...;
return eventAssembler.toResource(event, pageAssembler);
}
You provide the ResourceAssembler<Event> that tells Spring how to turn Event into a Resource. Spring injects the PagedResourcesAssembler<Event> into your controller method to handle the pagination links. Combine them by calling toResource and passing in the injected pageAssembler.
The final result can be returned simply as a body as above. You could also use things like HttpEntity to gain more control over status codes and headers.
Note: The ResourceAssembler you provide can literally be something as simple as wrapping the resource, such as Event, with a Resource object. Generally you'll want to add any relevant links though.
To hack it you can use just PagedResourcesAssembler<Object> like:
#RequestMapping(method = RequestMethod.GET, value = "")
#ResponseBody
public PagedModel<PersistentEntityResource> getEvents(
Pageable pageable,
PersistentEntityResourceAssembler persistentAssembler,
PagedResourcesAssembler<Object> pageableAssembler
) {
return pageableAssembler.toModel(
(Page<Object>) repository.findAll(pageable),
persistentAssembler
);
}

Spring REST ambigious method

I need a spring rest controller for inserting data. This is what i already got:
#RestController
#Transactional
public abstract class AbstractRESTController<E extends Identifiable<P>, P extends Serializable> {
#RequestMapping(method = RequestMethod.POST, consumes=MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(HttpStatus.CREATED)
public void create(#RequestBody final E entity) {
service.create(entity);
}
}
So i am able to insert a entity at http://mycontext/
What i need now is a method which accepts a list of entitys at the same path. Basiclly this:
#RequestMapping(method = RequestMethod.POST, consumes=MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(HttpStatus.CREATED)
public void createAll(#RequestBody final List<E> entities) {
for (E entity : entities) {
service.create(entity);
}
}
So how can i make spring aware of that im sending a array and not a single entity and then use the other function?
My error:
Caused by: java.lang.IllegalStateException: Ambiguous mapping found. Cannot map 'sfusersRESTController' bean method
public void AbstractRESTController.createAll(java.util.List<E>)
to {[/sfusers],methods=[POST],params=[],headers=[],consumes=[application/json],produces=[],custom=[]}: There is already 'sfusersRESTController' bean method
public void AbstractRESTController.create(E) mapped.
If you want to map more than one request to a given path, you will have to use different HTTP methods; eg. POST, PUT.
In your situation, I would make the URLs different; i.e. one /mycontext/as-entity and /mycontext/as-list.
Or you MUST have the same URL - it must be able to handle all kinds of request bodies. So you could have one RequestMapping() which expects an Object - and then handle that Object - either as an Entity or a List.
Personally, I would still prefer different RequestMapping paths.
define different request mapping paths so that it can make corresponding callback using
#RequestMapping(value = "")
Now your rest controller will look like this:
#RestController
#Transactional
public abstract class AbstractRESTController<E extends Identifiable<P>, P extends Serializable> {
#RequestMapping(value = "create", method = RequestMethod.POST, consumes=MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(HttpStatus.CREATED)
public void create(#RequestBody final E entity) {
service.create(entity);
}
#RequestMapping(value = "createAll", method = RequestMethod.POST, consumes=MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(HttpStatus.CREATED)
public void createAll(#RequestBody final List<E> entities) {
for (E entity : entities) {
service.create(entity);
}
}
}

Categories