How to use #Api swagger annotation in sub-resource class? - java

In my project I have some sub-resources, correctly implemented according to the Jersey framework guidelines.
However, I have a problem in generating the openapi.json file (generated by swagger-maven-plugin).
In particular, I would like to be able to use the #Api swagger annotation on classes that implement sub-resources, to apply some properties, such as authorizations.
The problem is that if I use the #Api annotation on the sub-resource classes, swagger sees those classes not only as a sub-resources, but also as resources. The result is that in the openapi.json file, for each sub-resource, a same resource is generated (which should not exist).
The root resource:
#Path("root")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
#Api(value = "/root", authorizations = {
#Authorization(value = "auth", scopes = {})
})
public class ExperienceResource {
#Path("/sub_a")
public SubResourceA getSubA() {
return new SubResourceA();
}
#Path("/sub_b")
public SubResourceB getSubB() {
return new SubResourceB();
}
}
The sub-resource:
#Api(authorizations = {
#Authorization(value = "auth", scopes = {})
})
public class SubResourceA {
#GET
...
}
I also tried using #Api(value="") or #Api(value="sub_a"), but they don't work or make things worse.
Obviously, if I remove the #Api annotation everything works correctly, but I am forced to apply the properties operation by operation.
Any suggestions?

The solution was quite simple. To ensure that the class that implements the sub-resource is seen by swagger just as a sub-resource (and not as a resource too) just add the property hidden = true in the #Api annotation
#Api(hidden = true, authorizations = {
#Authorization(value = "auth", scopes = {})
})
public class SubResourceA {
#GET
...
}

Related

Java - How do I get OpenAPI (Swagger) to recognise #BeanParam for path and query params?

I'm dealing with Swagger doc generation for Java APIs that reuse a lot of common path structure across the board, so we've created a number of #BeanParam classes which hold #PathParams to help manage this.
We also have several #BeanParam classes which encapsulate multiple #QueryParams in one object, e.g. for a search request.
Here's an example of one API and a #BeanParam. This demonstration is a class holding #PathParams, but the problems are exactly the same with the #QueryParam-holding classes.
#POST
#Path("/entry/region/{regionId}/folder/{folderId}/entries")
#Consumes(APPLICATION_JSON)
#Produces(APPLICATION_JSON)
#Operation(operationId = "createEntry", description = "Create an entry")
#Tag(name = "external")
#RequestBody(content = #Content(mediaType = APPLICATION_JSON, schema = #Schema(ref = "EntryRequest")))
#APIResponse(responseCode = "201", ref = "Created")
#APIResponse(responseCode = "400", ref = "BadRequest")
public Response createEntry(#BeanParam EntryPath path,
#NotNull #Valid EntryRequest request) {
return entryCreateService.createEntry(path, request);
}
And here's the class used as the #BeanParam:
public class EntryPath {
#Parameter(in = ParameterIn.PATH)
#PathParam("regionId")
protected String regionId;
#Parameter(in = ParameterIn.PATH)
#PathParam("folderId")
protected String folderId;
public String getRegionId() {
return regionId;
}
public String getFolderId() {
return folderId;
}
}
This is an example; we've got various other #BeanParams going up to three or four path parameters (and some including #QueryParams on top of these).
Now when generating an OpenAPI / Swagger file using the code generator, the #BeanParam isn't picked up on at all. I expect to find the path parameters it encapsulates included. Instead, what we see is this:
paths:
/entry/region/{regionId}/folder/{folderId}/entries:
post:
tags:
- external
description: Create an entry
operationId: createEntry
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/EntryRequest'
responses:
"201":
$ref: '#/components/responses/Created'
"400":
$ref: '#/components/responses/BadRequest'
...
The path params inside the 'EntryPath' BeanParam should be included here, but they've been passed over completely. Whereas, when we do use a simple #PathParam in the method signature (in other APIs), there's no issue.
I've tried several things to fix this, like
annotating the BeanParam classes with #Schema on the class and its fields (as we do for request body classes),
adding more fields to the #PathParam annotations inside the BeanParam class: e.g. #Parameter(in = ParameterIn.PATH, name = "regionId", required = true)
sticking #ApiModel and/or #ApiModelProperty on the BeanParam in the method signature and the BeanParam itself,
adding swagger-jersey2-jaxrs as a dependency as suggested elsewhere online,
combinations of these
No success.
Really hoping someone can offer help on this - it's been a bit of a dead end so far.

Quarkus & Microprofile : Is there a better way to use a property from application.properties into #ClientHeaderParam?

I'm trying to build a simple app that calls an API with quarkus-rest-client.
I have to inject an API Key as a header which is the same for all resources of the API.
So I would like to put the value of this API Key (that depends on the environment dev/qa/prod) in the application.properties file located in src/main/resources.
I tried different ways to achieve this:
Use directly com.acme.Configuration.getKey into #ClientHeaderParam value property
Create a StoresClientHeadersFactory class which implements ClientHeadersFactory interface to inject the configuration
Finally, I found the way described below to make it work.
My question is: Is there a better way to do it?
Here's my code:
StoreService.java which is my client to reach the API
#Path("/stores")
#RegisterRestClient
#ClientHeaderParam(name = "ApiKey", value = "{com.acme.Configuration.getStoresApiKey}")
public interface StoresService {
#GET
#Produces("application/json")
Stores getStores();
}
Configuration.java
#ApplicationScoped
public class Configuration {
#ConfigProperty(name = "apiKey.stores")
private String storesApiKey;
public String getKey() {
return storesApiKey;
}
public static String getStoresApiKey() {
return CDI.current().select(Configuration.class).get().getKey();
}
}
StoresController.java which is the REST controller
#Path("/stores")
public class StoresController {
#Inject
#RestClient
StoresService storesService;
#GET
#Produces(MediaType.APPLICATION_JSON)
public Stores getStores() {
return storesService.getStores();
}
}
Late to the party, but putting this here for my own reference. There seems to be a difference in using #ClientHeaderParam and #HeaderParam, so I investigated a little further:
According to the Microprofile docs, you can put a compute method for the value in curly braces. This method can extract the property value.
See link for more examples.
EDIT: What I came up with resembles the original, but uses a default method on the interface, so you can at least discard the Configuration class. Also, using the org.eclipse.microprofile.config.Config and ConfigProvider classes to get the config value:
#RegisterRestClient
#ClientHeaderParam(name = "Authorization", value = "{getAuthorizationHeader}")
public interface StoresService {
default String getAuthorizationHeader(){
final Config config = ConfigProvider.getConfig();
return config.getValue("apiKey.stores", String.class);
}
#GET
#Produces("application/json")
Stores getStores();
I will get rid of the Configuration class and use an #HeaderParam to pass your configuration property from your rest endpoint to your rest client. The annotation will then send this property as an HTTP header to the remote service.
Somthing like this should works:
#Path("/stores")
#RegisterRestClient
public interface StoresService {
#GET
#Produces("application/json")
Stores getStores(#HeaderParam("ApiKey") storesApiKey);
}
#Path("/stores")
public class StoresController {
#ConfigProperty(name = "apiKey.stores")
private String storesApiKey;
#Inject
#RestClient
StoresService storesService;
#GET
#Produces(MediaType.APPLICATION_JSON)
public Stores getStores() {
return storesService.getStores(storesApiKey);
}
}

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

Spring hateoas xml serialization for list of resources built with ResourceAssemblerSupport

I am trying to support XML responses for my Spring HATEOAS based application. JSON responses work fine as well as XML for a single resource. The problem starts with the list of the resources. Spring MVC controller cannot serialize the list built with help of ResourceAssemblerSupport derived class. The controller throws "org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation" for the curl command
curl -k -i -H "Accept:application/xml" -H "Media-Type:application/xml" -X GET http://127.0.0.1:8080/admin/roles*
My HATEOAS resource is a wrapper around entity class:
#XmlRootElement
#XmlSeeAlso(RoleModel.class)
public class RoleResource extends ResourceSupport {
public RoleModel role;
}
The controller is simple:
#RequestMapping(method = RequestMethod.GET)
public #ResponseBody HttpEntity<List<RoleResource>> getAllRoles()
throws ObjectAccessException, ObjectNotFoundException {
List<RoleModel> resp = rolesManagement.getRoles();
return new ResponseEntity<List<RoleResource>>(roleResourceAssembler.toResources(resp),
HttpStatus.OK);
}
Resource assembler class:
#Configuration
public class RoleResourceAssembler extends ResourceAssemblerSupport<RoleModel, RoleResource> {
public RoleResourceAssembler() {
super(RolesRestController.class, RoleResource.class);
}
#Bean
public RoleResourceAssembler roleResourceAssembler(){
return new RoleResourceAssembler();
}
#Override
public RoleResource toResource(RoleModel role) {
RoleResource res = instantiateResource(role);
res.role = role;
try {
res.add(linkTo(methodOn(RolesRestController.class).getRole(role.getRoleId())).withSelfRel());
} catch (ObjectAccessException | ObjectNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return res;
}
}
When I avoid ResourceAssemblerSupport and build my resources manually like this:
#XmlRootElement
#XmlSeeAlso(RoleModel.class)
public class RolesList {
private List<Resource<RoleModel>> roles;
...
}
#RequestMapping(method = RequestMethod.GET)
public #ResponseBody HttpEntity<RolesList> getAllRoles()
throws ObjectAccessException, ObjectNotFoundException {
List<RoleModel> resp = rolesManagement.getRoles();
List<Resource<RoleModel>> roles =new ArrayList<>();
for (RoleModel model: resp) {
Resource<RoleModel> res = new Resource<RoleModel>(model);
res.add(linkTo(methodOn(RolesRestController.class).getRole(model.getRoleId())).withSelfRel());
roles.add(res);
}
RolesList list = new RolesList();
list.setRoles(roles);
return new ResponseEntity<RolesList>(list,
HttpStatus.OK);
}
XML serialization works. I guess I could avoid using resource assembler and build my resources manually, but that makes the code not as clean and modular. I wonder if it is still possible to use ResourceAssemblerSupport as resource builder and return the list of resources as XML
A resource assembler is for converting one pojo/entity/whatever to 1 hateoas resource. You are trying to convert a list to a list.
If you assembler was
public class RoleResourceAssembler extends ResourceAssemblerSupport<List<RoleModel>, RolesResource> {
and RolesResource was something that you wanted...it should work.
However i would suggest you look at using the PagedResourceAssembler which takes a page of "things" and uses an assembler to create a page of resources. The page can be the full collection or just a page in the collection. Here's a simple one for categories:
public HttpEntity<PagedResources<CategoryResource>> categories(
PagedResourcesAssembler<Category> pageAssembler,
#PageableDefault(size = 50, page = 0) Pageable pageable
){
Page<Category> shopByCategories = categoryService.findCategories(pageable);
PagedResources<CategoryResource> r = pageAssembler.toResource(shopByCategories,
this.categoryAssembler);
return new ResponseEntity<PagedResources<CategoryResource>>(r, HttpStatus.OK);
}
But..i've had some problems with jackson marshaling the PagedResources as XML...works fine for JSON.
It seems that with ResourceAssemblerSupport it is impossible to marshal list of HATEOAS resources to XML. The reason is because the list of resources returned by toResources() method of the class extending ResourceAssemblerSupport has no #XmlRootElement and JAXB fails to marshal it. I had to create classes like
#XmlRootElement
public class Roles {
private List<RoleResource> roleResource;
....
}
#XmlRootElement
public class RoleResource extends ResourceSupport {
private RoleModel role;
...
}
and manually build my resource list. The same problem occurred when I tried to use to use Spring HATEOAS resource wrapper like
Resource<RoleModel> resource = new Resource<>();
Since Spring's Resource class is not annotated with #XmlRootElement REST controller is unable to marshal it to XML
If your only requirement is to generate links with org.springframework.hateoas.Link and marshall as XML, the following may help.
Add a Link item to your model class.
#XmlRootElement(name="Role")
public class Roles
{
Link link;
<your Roles content>
...
}
Wrap the base class in a list in order to provide a base tag which supports XML marshalling.
#XmlRootElement(name="Roles")
public class RolesList
{
private List<Roles> rolesList;
...
<constructors>
...
#XmlElement(name="Role")
public List<Roles> getRolesList()
{
return rolesList;
}
<set/get/add methods>
}
Your controller code becomes (roughly):
#RequestMapping(method = RequestMethod.GET)
public #ResponseBody HttpEntity<RolesList> getAllRoles()
throws ObjectAccessException, ObjectNotFoundException
{
RolesList resp = new RolesList(rolesManagement.getRoles());
for (Roles r: resp)
{
r.setLink(linkTo(methodOn(RolesRestController.class).getRole(model.getRoleId())).withSelfRel());
}
return new ResponseEntity<RolesList>(resp,HttpStatus.OK);
}

Categories