Spring request mapping custom annotations - ambiguous mapping - java

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.

Related

Spring custom annotation validate http header with reflection

I have an endpoint secured with token given in the header. I would like to create my custom annotation so when I annotate the controller method the validation token goes first and then if the token was accurate do the rest of the method. I tried do it with interceptor and it worked but I want to do it using reflection to have custom annotation. I can share some of my code snippet but there is not a lot of code because I couldn't find some that tells me how to get token from header and validate it. And I'm a noob in reflection.
Custom Annotation
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface TokenValidation {
String token() default "";
}
Controller Class
#TokenValidation
#PostMapping("/endpoint")
public ResponseEntity createComplianceDirectory(#RequestBody ComplianceDirRequest request) {
return ResponseEntity.status(HttpStatus.OK).build();
}
Reflection class
#Value("${token}")
private String token;
private Reflections reflections;
public Reflection() {
reflections = new Reflections(new ConfigurationBuilder().setUrls(ClasspathHelper.forPackage("com.sampleapp.controller"))
.setScanners(new FieldAnnotationsScanner()));
setReflections();
}
public void setReflections() {
Set<Method> methods = reflections.getMethodsAnnotatedWith(TokenValidation.class);
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
String requestToken = request.getHeader("Authorization");
}
}
Could someone tell me:
How to register that reflection class in spring boot to be invoked when the controller method is called.
Is it a good way in my reflection class to retrieve the header ?

How to set defaults for #RequestMapping?

I'm using #RestController with #RequestMapping annotations to define all of my servlets with spring-mvc.
Question: how can I define some defaults for those annotation, so I don't have to repeat the same configuration regarding eg consumes and produces?
I'd like to always apply the following config, without having to repeat it on each path:
#GetMapping(produces = {APPLICATION_XML_VALUE, APPLICATION_JSON_VALUE})
#PostMapping(
consumes = {APPLICATION_XML_VALUE, APPLICATION_JSON_VALUE},
produces = {APPLICATION_XML_VALUE, APPLICATION_JSON_VALUE})
Probably it's easiest to just create a custom #RestController annotation and use that on classlevel. Then I only have to repeat the #PostMapping(consumes...) mappings:
#Target(ElementType.TYPE)
#Retention(value=RUNTIME)
#RestController
#RequestMapping(produces = {APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE})
public #interface DefaultRestController {
}
Usage like:
#DefaultRestController
public class MyServlet {
#GetMapping("/getmap") //inherits the 'produces' mapping
public void getmap() {
}
#PostMapping("/postmap", consumes = {APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE})
public void postmap() {
}
}
Better than nothing.
The target for RequestMapping annotation could be either a method or class. It can be used instead of GetMapping and PostMapping annotations that target only methods.
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/GetMapping.html
Specifically, #GetMapping is a composed annotation that acts as a
shortcut for #RequestMapping(method = RequestMethod.GET).
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/PostMapping.html
Specifically, #PostMapping is a composed annotation that acts as a
shortcut for #RequestMapping(method = RequestMethod.POST).
Assuming your Controller Name is HelloController, add the RequestMapping annotation with appropriate methods at Class level so that it applies automatically to all the paths.
#Controller
#RequestMapping(method={RequestMethod.GET,RequestMethod.POST}, consumes = { MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE },produces = { MediaType.APPLICATION_XML_VALUE,MediaType.APPLICATION_JSON_VALUE },)
class HelloController{
}
This confgiuration can be overridden by annotating it in individual methods.
You can put an annotation on class. Here is an example:
#RestController
#RequestMapping(
consumes = {APPLICATION_XML_VALUE, APPLICATION_JSON_VALUE},
produces = {APPLICATION_XML_VALUE, APPLICATION_JSON_VALUE}
)
public class MyClass {
// after that you don't have to put any
// #RequestMapping default values before methods
}

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 programmatically register RequestMapping

I have a working url as this: localhost/info
#Controller
#RequestMapping("/info")
public class VersionController {
#RequestMapping(value = "", method = RequestMethod.GET)
public #ResponseBody
Map get() {
loadProperties();
Map<String, String> m = new HashMap<String, String>();
m.put("buildTimestamp", properties.getProperty("Application-Build-Timestamp"));
m.put("version", properties.getProperty("Application-Version"));
return m;
}
}
and I would to register some other mappings at initializing of my application as this:
localhost/xxxx/info
localhost/yyyy/info
localhost/zzzz/info
All these urls will return same response as localhost/info
The xxxx, yyyy part of the application is changeable. I have to register custom mappings as
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("???").setViewName("???");
}
Bu this is only working for views.
Any idea for dynamic registration?
You can register a new HandlerMapping where you can add the handlers for your URL paths; the most convenient implementation would be SimpleUrlHandlerMapping.
If you want those handlers to be bean methods (like those annotated with #RequestMapping) you should define them as HandlerMethod wrappers so that the already registered RequestMappingHandlerAdapter will invoke them.
As of Spring 5.0.M2, Spring provides a functional web framework that allows you to create "controller"-like constructs programmatically.
What you would need to do in your case is create a proper RouterFunction for the URLs you need to handle, and then simply handle the request with the appropriate HandlerFunction.
Keep in mind however that these constructs are not part of Spring MVC, but part of Spring Reactive.
Check out this blog post for more details
I believe that simple example is worth more than 1000 words :) In SpringBoot it will look like...
Define your controller (example health endpoint):
public class HealthController extends AbstractController {
#Override
protected ModelAndView handleRequestInternal(#NotNull HttpServletRequest request, #NotNull HttpServletResponse response) {
return new ModelAndView(new MappingJacksonSingleView(), "health", Health.up().build());
}
}
And create your configuration:
#Configuration
public class CustomHealthConfig {
#Bean
public HealthController healthController() {
return new HealthController();
}
#Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(Integer.MAX_VALUE - 2);
mapping.setUrlMap(ImmutableMap.of("/health", healthController()));
return mapping;
}
}
Regarding the handler order - take a look about the reason here: Java configuration of SimpleUrlHandlerMapping (Spring boot)
The MappingJacksonSingleView is auxiliary class in order to return single json value for model and view:
public class MappingJacksonSingleView extends MappingJackson2JsonView {
#Override
#SuppressWarnings("unchecked")
protected Object filterModel(Map<String, Object> model) {
Object result = super.filterModel(model);
if (!(result instanceof Map)) {
return result;
}
Map map = (Map) result;
if (map.size() == 1) {
return map.values().toArray()[0];
}
return map;
}
}
Jackson single view source - this blog post: https://www.pascaldimassimo.com/2010/04/13/how-to-return-a-single-json-list-out-of-mappingjacksonjsonview/
Hope it helps!
It is possible (now) to register request mapping by RequestMappingHandlerMapping.registerMapping() method.
Example:
#Autowired
RequestMappingHandlerMapping requestMappingHandlerMapping;
public void register(MyController myController) throws Exception {
RequestMappingInfo mappingInfo = RequestMappingInfo.paths("xxxx/info").methods(RequestMethod.GET).build();
Method method = myController.getClass().getMethod("info");
requestMappingHandlerMapping.registerMapping(mappingInfo, myController, method);
}
You could register your own HandlerMapping by extending the RequestMappingHandlerMapping, e.g. override the registerHandlerMethod.
It's not quite clear what you're trying to achieve, but maybe you can use #PathVariable in your #RequestMapping, something like:
#RequestMapping("/affId/{id}")
public void myMethod(#PathVariable("id") String id) {}
Edit: Original example has changed it appears, but you might be able to use PathVariable anyway.

Categories