Spring Data Rest / Spring Hateoas Custom Controller - PersistentEntityResourceAssembler - java

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
);
}

Related

How to map custom/dynamic requests to a controller (Spring)?

How can I map custom/dunamic requests toa given controller, based on a repository lookup?
The use-case is a CMS-like feature in a web-platform, where certain URL patterns ("pages") stored in the DB should be handled by a separate controller PageController.java. These patterns are not necessarily known at compile-time, and they can also be added and modified while the app is deployed (thus, it cannot be annotation-driven).
I did try to map a controller to "**" (see below), but that did not work for 2 reasons: firstly all other requests resolved to that same controller method (I had hoped that it would use "**" as a fallback and try the others first), and it also ended up resolving all requests to my static/asset files to this controller (resulting in unwanted 404-responses).
#Controller
public class PageController {
#Inject
private PageService pageService;
#RequestMapping(value = "**", method = RequestMethod.GET)
public String getPage(Model model, HttpServletRequest request, #CurrentUser User user) {
String path = request.getRequestURI();
Page page = this.pageService.getByPath(path, user);
if (page == null) {
throw new NotFoundException();
}
model.addAttribute("page", page);
return "web/page";
}
}
The temporary work-around/modification to the above method has so far been to map a pre-defined URL-prefixes to this controller (eg. /page/**, /info/**, /news/** etc), but this is an inelegant solution that adds arbitrary limitations to the system which I now seek to eliminate.
I am currently using Spring Boot 2.0. In addition to the naive mapping to ** in a regular #Controller class (using the #RequestMapping -annotation), I have also tried configuring SimpleUrlHandlerMapping the following way:
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Inject
private PageDao pageDao;
#Bean
public PageController pageController() {
return new PageController();
}
#Bean
public SimpleUrlHandlerMapping pageUrlHandlerMapping() {
SimpleUrlHandlerMapping pageUrlHandlerMapping = new SimpleUrlHandlerMapping();
PageController pageController = this.pageController();
Map<String, Object> urlMap = this.pageDao.findAll().stream()
.map(Page::getNormalizedSlug)
.collect(Collectors.toMap(Function.identity(),
slug -> pageController, (existing, duplicate) -> existing));
pageUrlHandlerMapping.setUrlMap(urlMap);
pageUrlHandlerMapping.setOrder(Ordered.HIGHEST_PRECEDENCE); // <- Cannot be LOWEST_PRECEDENCE for some reason...
return pageUrlHandlerMapping;
}
}
public class PageController implements Controller {
#Inject
private PageService pageService;
#Inject
private DmaWebControllerAdvice controllerAdvice;
#Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
User user = null;
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof User) {
user = (User) principal;
}
String path = request.getRequestURI();
Page page = this.pageService.getByPath(path, user);
if (page == null) {
throw new NotFoundException();
}
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("web/page");
modelAndView.addObject("page", page);
controllerAdvice.globalModelAttributes(modelAndView.getModel(), null);
return modelAndView;
}
}
This approach does technically work, but the list of pages will somehow have to be reloaded into the SimpleUrlHandlerMapping whenever one of the pages is changed (i am not quite sure how to do that). This also possibly overwrites some default Spring Boot-configuration, that I would ideally like to keep. It also has some drawbacks compared to resolving controllers using #Controller and #RequesMapping because I currently am injecting certain data into all views resolved that way (mainly model-data used in the overall design of the website, like menu, quicklinks etc). In the above attempt, I have had to set those via a separate call to controllerAdvice-globalModelAttributes().
What I am seeking is a solution where my repository is queried for potential page-matches in runtime, and if it is valid then the request will be handled by the proper page-controller. Is a custom HandlerMapping -implementation the way to do this? And if not, how should I solve this? And if making a separate HandlerMapping for pages, how do I add/register this in my configuration without overwriting the default provided by Spring?
Why don't you just implement a catch-all controller which parses your patterns as a parameter, does a db look-up and then use a forward to specific controllers (info, page, news etc.)? Seems like for a CMS, this look-up logic belongs into your code (e.g. service layer).
Easiest(but not the best) way to achieve what you need is creating custom HandlerMapping implementation:
public class PageMapper implements HandlerMapping, Ordered {
private HandlerMethod handlerMethod;
public CustomMapper(Object controller, Method method) {
this.handlerMethod = new HandlerMethod(controller, method);
}
#Override
public HandlerExecutionChain getHandler(HttpServletRequest httpServletRequest) throws Exception {
return new HandlerExecutionChain(handlerMethod);
}
#Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE; //you have to add the handler to the end
}
}
Now remove #Controller annotation from PageController because you don't need it to be detected automatically anymore. After that register controller and mapping to config:
#Configuration
public class AppWebConfig implements WebMvcConfigurer {
#Bean
public PageController pageController() {
return new PageController();
}
#Bean
public HandlerMapping pageMapping(PageController pageController) {
Method method = BeanUtils.resolveSignature("getPage", PageController.class);
return new PageMapping(pageController, method);
}
}
Now every request unrecognized by other HandlerMapping instances will be sent to your mapping hence to your controller. But this approach has obvious disadvantage. Since your mapping is the last in the chain of mappings you never get 404 error. Therefor you never know about something wrong with you resources (e.g. if some of them are missing).
I would prefer let application to distinguish paths by prefix (just like you do it already), where prefix is operation application is going to do with a page. For example if you need to show or edit the page:
#Controller
public class PageController {
private final static String SHOW = "/show";
private final static String EDIT = "/edit";
#Inject
private PageService pageService;
GetMapping(value = SHOW + "/**")
public String getPage(Model model, HttpServletRequest request, #CurrentUser User user) {
String path = request.getRequestURI().substring(SHOW.length());
Page page = this.pageService.getByPath(path, user);
...
model.addAttribute("page", page);
return "web/page";
}
//the same for EDIT operation
}

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);
}
}

Spring MVC Ignore Json property for a given controller method

I have a Java class (MyResponse) that is returned by a multiple RestController methods and has a lot of fields.
#RequestMapping(value = "offering", method=RequestMethod.POST)
public ResponseEntity<MyResponse> postOffering(...) {}
#RequestMapping(value = "someOtherMethod", method=RequestMethod.POST)
public ResponseEntity<MyResponse> someOtherMethod(...) {}
I want to ignore (e.g. not serialize it) one of the properties for just one method.
I don't want to ignore null fields for the class, because it may have a side effect on other fields.
#JsonInclude(Include.NON_NULL)
public class MyResponse { ... }
The JsonView looks good, but as far as I understand I have to annotate all other fields in the class with a #JsonView except the one that I want to ignore which sounds clumsy. If there is a way to do something like "reverse JsonView" it will be great.
Any ideas on how to ignore a property for a controller method?
Props to this guy.
By default (and in Spring Boot) MapperFeature.DEFAULT_VIEW_INCLUSION is enabled in Jackson. That means that all fields are included by default.
But if you annotate any field with a view that is different than the one on the controller method this field will be ignored.
public class View {
public interface Default{}
public interface Ignore{}
}
#JsonView(View.Default.class) //this method will ignore fields that are not annotated with View.Default
#RequestMapping(value = "offering", method=RequestMethod.POST)
public ResponseEntity<MyResponse> postOffering(...) {}
//this method will serialize all fields
#RequestMapping(value = "someOtherMethod", method=RequestMethod.POST)
public ResponseEntity<MyResponse> someOtherMethod(...) {}
public class MyResponse {
#JsonView(View.Ignore.class)
private String filed1;
private String field2;
}

Spring request mapping custom annotations - ambiguous mapping

I have the following Spring MVC Controller:
#RestController
#RequestMapping(value = "my-rest-endpoint")
public class MyController {
#GetMapping
public List<MyStuff> defaultGet() {
...
}
#GetMapping(params = {"param1=value1", "param2=value2"})
public MySpecificStuff getSpecific() {
...
}
#GetMapping(params = {"param1=value1", "param2=value3"})
public MySpecificStuff getSpecific2() {
return uiSchemas.getRelatedPartyUi();
}
}
What I need is to make it more generic using custom annotations:
#RestController
#RequestMapping(value = "my-rest-endpoint")
public class MyController {
#GetMapping
public List<MyStuff> defaultGet() {
...
}
#MySpecificMapping(param2 = "value2")
public MySpecificStuff getSpecific() {
...
}
#MySpecificMapping(param2 = "value3")
public MySpecificStuff getSpecific2() {
return uiSchemas.getRelatedPartyUi();
}
}
I know that Spring meta annotations could help me with that.
So I define the annotation:
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#RequestMapping(method = RequestMethod.GET, params = {"param1=value1"})
public #interface MySpecificMapping {
String param2() default "";
}
That alone won't do the trick.
So I add an interceptor to deal with that "param2":
public class MyInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// get annotation of the method
MySpecificMapping mySpecificMapping = handlerMethod.getMethodAnnotation(MySpecificMapping.class);
if (mySpecificMapping != null) {
// get the param2 value from the annotation
String param2 = mySpecificMapping.param2();
if (StringUtils.isNotEmpty(param2)) {
// match the query string with annotation
String actualParam2 = request.getParameter("param2");
return param2 .equals(actualParam2);
}
}
}
return true;
}
}
And include it into the Spring configuration of course.
That works fine but only if I have one such custom mapping per controller.
If I add two methods annotated with #MySpecificMapping even having different values of "param2" then I get an "ambiguous mapping" error of the application start:
java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'myController' method
public com.nailgun.MySpecificStuff com.nailgun.MyController.getSpecific2()
to {[/my-rest-endpoint],methods=[GET],params=[param1=value1]}: There is already 'myController' bean method
public com.nailgun.MySpecificStuff com.nailgun.MyController.getSpecific() mapped.
at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry.assertUniqueMethodMapping(AbstractHandlerMethodMapping.java:576)
- Application startup failed
I understand why it happens.
But can you help me to give Spring a hint that those are two different mappings?
I am using Spring Boot 1.4.3 with Spring Web 4.3.5
#AliasFor is annotation for do things like this.
Here is an example of custom annotation with #RequestMapping
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
#RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public #interface JsonGetMapping {
#AliasFor(annotation = RequestMapping.class, attribute = "value")
String value() default "";
}
and also example of use
#JsonGetMapping("/category/{categoryName}/books")
public List<Book> getBooksByCategory(#PathVariable("categoryName") String categoryName){
return bookRepository.getBooksByCategory(categoryName);
}
You can not bind annotations in the stack with their params and Spring will consider these two methods as methods with equal #RequestMapping.
But you could try make a trick: embed somehow your custom annotation enhancer before mapping builder and perform there annotations replacing:
Get all methods with annotation #MySpecificMapping:
MySpecificMapping myMapping = ...;
Read #RequestMapping annotation for each such method, let say it will be
RequestMapping oldMapping = ...;
Create new instance of the #RequestMapping class:
RequestMapping newMapping = new RequestMapping() {
// ... rest methods
#Override
public String[] params() {
// here merge params from old and MySpecificMapping:
String[] params = new String[oldMapping.params().length + 1];
// todo: copy old one
// ...
params[params.length-1] = myMapping.param2();
return params;
}
}
Forcly assign this new newMapping to each method correspondingly instead of oldMapping.
This is quite tricky and complex, but this is only one way to achieve what you want, I believe.
I think the best way around this is to move your #RequestMapping annotation to the method level instead of the class level.
The error Spring is giving you is because Spring is binding multiple handlers on one path which is invalid. Maybe give us an example of the URL's you'd like to expose so we have a better overview of what you're trying to build.

Call method of Controller1 into another method of Controller2 in Spring

I did Google a lot to find my problem but I couldn't and sorry If this question already on the stack overflow because I have not find it.
First let take a look into the code
#Controller
public class Controller1 {
#RequestMapping(value = "URL", method = RequestMethod.GET)
public ModelAndView methodHandler(Parameters) {
}
public int calculation(int i){
//Some Calcucation
return i;
}
}
and second controller is
#Controller
public class Controller2 {
#RequestMapping(value = "URL", method = RequestMethod.GET)
public ModelAndView methodHandler(Parameters) {
//In this I want to call the calculation(1) method of controller1.
}
}
My question is that is there any way to call the method of calculation() of controler1 in to controller2. But remember I don't want to make method static in controller1.Is there anyway to call it without make it static?
Thanks
Yasir
You should create service bean for example in configuration file (or use # one of the annotaions) and inject it into controller. For example ()
#Configuration
public class MyConfig {
#Bean
public MyService myService(){
return new MyService();
}
}
#Controller
public class Controller1 {
#Autowire
private MyService myService;
#RequestMapping(value = "URL", method = RequestMethod.GET)
public ModelAndView First(Parameters) {
myService.calculation();
}
}
#Controller
public class Controller2 {
#Autowire
private MyBean myBean;
#RequestMapping(value = "URL", method = RequestMethod.GET)
public ModelAndView First(Parameters) {
myService.calculation();
}
}
Your controllers should not call each other. If there is a logic which needs to be used by both controllers, it is much better to put that into separate bean, which will be used by both controllers. Then you can simply inject that bean to whicheveer controller neccessary. Try not to put any business logic to controllers, try tu put it to specialized class instead which will be web independent if possible and will accept web agnostic business data as user email, account number etc. No http request or response. This way your class with actual logic is reusable and can be unit tested much more easily. Also, if there is state, it should be contained in your classes outside controllers. Controllers should be stateless and not contail any state at all.
When using MVC pattern and you are deciding where to put your logic, you should separate business logic into model and into controllers you should put only logic regarding user interaction, as explained in this stack overflow post.

Categories