File Upload on Swagger Controller Interface - java

I'm creating an API micro-service that provides file storage with AWS. I'm putting together the Swagger and Controller, and we need to be able to allow users to upload a file on the Swagger. The problem is our controller is set up as an interface instead of a class, and solutions from Google/SO aren't making the cut to be able to work with interfaces. To clarify, I don't need to manipulate the file at all, just take it in. Our internal implementation methods will take then send it off to S3.
This is using Java 11, AWS S3, Spring Boot, and Swagger 2. I've tried using #ApiParam and #FormDataParam inside the method createFile, but I've gotten two different errors:
method must be abstract
annotations are not allowed here.
#RequestMapping(value = {"v3/registration/documents", "v4/registration/documents"})
#RestController
#Api(
value = "file-storage",
description = "File storage service",
tags = {"file-storage"})
public interface FileController {
#PostMapping(
value = "/{salesPlanAff}",
produces = {MediaType.APPLICATION_JSON_VALUE},
consumes = {MediaType.APPLICATION_JSON_VALUE})
#ApiOperation(value = "Upload a file")
#ApiResponses(
value = {
#ApiResponse(code = 200, message = "Success", response = FileResponseDTO.class),
#ApiResponse(code = 201, message = "Created"),
#ApiResponse(code = 400, message = "Bad Request"),
#ApiResponse(code = 401, message = "Unauthorized"),
#ApiResponse(code = 403, message = "Forbidden"),
#ApiResponse(code = 404, message = "Not Found"),
#ApiResponse(code = 500, message = "Internal Server Error")
})
void createFile(
#PathVariable(required = true, name = "webSessionId") String webSessionId,
#PathVariable(required = false, name = "salesPlanAff") String salesPlanAff);
What I expected is to have a button on my swagger page allowing file upload, didn't quite expect this much difficulty in adding file upload.

I have a FileUpload in my swagger page and it works like a charm. The only difference from your is that I am not doing it on an interface...
import org.springframework.web.multipart.MultipartFile;
...
#ApiOperation(value = "Analyse the identifiers in the file")
#RequestMapping(value = "/form", method = RequestMethod.POST, produces = "application/json")
#ResponseBody
public AnalysisResult getPostFile( #ApiParam(name = "file", value = "The file")
#RequestPart MultipartFile file,
HttpServletRequest r) {
UserData ud = controller.getUserData(file);
return controller.analyse(ud, r, file.getOriginalFilename());
}
I trimmed off a little bit this code, but you can find a the original one in our repository
Also, working version of this code can be executed tested here
Thanks

I figured out how to get the annotations in, part of it stemmed from typos. For anyone interested, here's the solution:
public interface FileController {
#PostMapping(
value = "/{salesPlanAff}",
produces = {MediaType.APPLICATION_JSON_VALUE},
consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
#ApiOperation(value = "Upload a file")
#ApiResponses(
value = {
#ApiResponse(code = 200, message = "Success", response = FileResponseDTO.class),
#ApiResponse(code = 201, message = "Created"),
#ApiResponse(code = 400, message = "Bad Request"),
#ApiResponse(code = 401, message = "Unauthorized"),
#ApiResponse(code = 403, message = "Forbidden"),
#ApiResponse(code = 404, message = "Not Found"),
#ApiResponse(code = 500, message = "Internal Server Error")
})
void createFile(
#PathVariable(required = true, name = "webSessionId") String webSessionId,
#PathVariable(required = false, name = "salesPlanAff") String salesPlanAff,
#ApiParam(required = true, value = "Document to be uploaded")
#RequestPart MultipartFile multipartFile,
#ApiParam(required = true, value = "File Type")
#QueryParam("documentType") String documentType);

Related

I have two APIs Interfaces and i want to implement both in a single controller, but controller gets hit for the 1st api interface methods only

#Path("/avManageThemesService")
#Api(description = "the avManageThemesService API")
#javax.annotation.Generated(value =
"org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2022-09-
02T19:11:34.805+05:30[Asia/Calcutta]")public interface AvManageThemesServiceApi {
#PUT
#Consumes({ "application/json" })
#Produces({ "application/json" })
#ApiOperation(value = "Update Theme", notes = "", tags={ "Update Theme" })
#ApiResponses(value = {
#ApiResponse(code = 200, message = "OK", response = SaveThemeResponse.class),
#ApiResponse(code = 400, message = "Bad Request", response = ErrorDetails.class),
#ApiResponse(code = 401, message = "Unauthorized", response = ErrorDetails.class) })
Response updateTheme(#Valid SaveThemeJSONRequest saveThemeJSONRequest);
}
#Path("/avListThemes")
#Api(description = "the avListThemes API")
#javax.annotation.Generated(value =
"org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2022-09-
02T19:11:34.805+05:30[Asia/Calcutta]")public interface AvListThemesApi {
#GET
#Produces({ "application/json" })
#ApiOperation(value = "Get all themes", notes = "", tags={ "getAllThemes" })
#ApiResponses(value = {
#ApiResponse(code = 200, message = "OK", response = GetThemesResponse.class),
#ApiResponse(code = 400, message = "Bad Request", response = ErrorDetails.class),
#ApiResponse(code = 401, message = "Unauthorized", response = ErrorDetails.class) })
Response getAllThemes();
}
public class Example implements AvManageThemesServiceApi, AvListThemesApi{
#Override
public Response updateTheme(SaveThemeJSONRequest aSaveThemeJSONRequest){
return Response.ok(200).build();
}
#Override
public Response getAllThemes(){
return Response.ok(200).build();
}
}
when i hit url from postman, it only gives response for the 1st implemented interface(here:AvManageThemesServiceApi) and gives response RESTEASY003210: Could not find resource for full path: for the other one(here:AvListThemesApi). Reverse happens when interfaces positions are exhanged.
Though when implemented with separate controllers, both works fine.

Example and description for #requestpart

The requirement is a user will upload files to spring boot (ReST request) using angular. The ReST interface store the file into gitlab for further processing. Using #RequestPart annotation, content with a different name will be retrieved from a request and placed into Gitlab. Implementation will look like as follows:
#ApiOperation(value = "Upload java files into Gitlab")
#RequestMapping(method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = "application/json")
#ApiResponses(value = { #ApiResponse(code = 200, response = CustomResponse.class, message = "Success"),
#ApiResponse(code = 400, response = ErrorResponse.class, message = "Bad Request"),
#ApiResponse(code = 500, response = ErrorResponse.class, message = "Internal Server Error") })
public CustomResponse uploadJava(
#ApiParam(value = "Interface", required = true)
#RequestPart(name = "interface", required = true) final MultipartFile interfaceClass,
#ApiParam(value = "Java Class", required = false)
#RequestPart(name = "javaClass", required = false) final MultipartFile javaClass) {
return null;
}
My question is,
How to add description and example for the above. (Similar to
RequestParam)
How to set content type for each file.
Note:
I tried modifying #RequestParam instead of #RequestPart. In this
case, the second file ("javaclass") alone pushed to spring boot.
My current POM
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
Regards

#ApiResponse with empty response body (Spring Boot)

I'm looking for a way to tell swagger that a certain API response code doesn't have a response body. A get response, for example, that can either return a 200 code with the actual object as a response or a 404 if the object associated with the passed ID doesn't exist:
#ApiResponses(value = {
#ApiResponse(responseCode = "200", description = "Object found"),
#ApiResponse(responseCode = "404", description = "Invalid object ID", content = #Content)
})
This is the closest thing I could figure out but it's not perfect, I still get an annoying "Media type" under the description of the 404 response.
Thanks!
If you are not specifying the content attribute of #ApiResponse annotation the return type of the controller method will be your response content. To prevent this define content explicitly:
#ApiResponse(responseCode = "200", description = "OK",
content = #Content(schema = #Schema(implementation = Void.class)))
Or you can simply return ResponseEntity<Void>.
This is probably the better (and shorter) way:
#ApiResponse(
responseCode = "404",
description = "Not found",
content = #Content(schema = #Schema(hidden = true)))
You can use the following on top of your method in v2
#ApiResponses(value = {
#ApiResponse(code = 200, message = "Success", response = YourObject.class),
#ApiResponse(code = 401, message = "Unauthorized"),
#ApiResponse(code = 403, message="Forbidden"),
#ApiResponse(code = 404, message = "Not Found"),
#ApiResponse(code = 500, message = "Failure")
})
For V3, you could try something like this in case your method is returning some object
#Operation(summary = "Add a new object", description = "", tags = { "yourObject" })
#ApiResponses(value = {
#ApiResponse(responseCode = "201", description = "Object created",content = #Content(schema = #Schema(implementation = YourObject.class))),
#ApiResponse(responseCode = "400", description = "Invalid input"),
#ApiResponse(responseCode = "409", description = "Object already exists") })
#PostMapping(value = "/your-url", consumes = {"application/json","application/xml" })
public ResponseEntity<YourObject> addObject(
...
return ...
}
In case your method is returning void try this one
#Operation(summary = "Update an existing object", description = "", tags = { "yourObject" })
#ApiResponses(value = {
#ApiResponse(responseCode = "200", description = "successful operation"),
#ApiResponse(responseCode = "400", description = "Invalid ID supplied"),
#ApiResponse(responseCode = "404", description = "Object not found"),
#ApiResponse(responseCode = "405", description = "Validation exception") })
#PutMapping(value = "/your-url/{id}", consumes = { "application/json", "application/xml" })
public ResponseEntity<Void> addObject(
...
return ...
}
Not sure if it's a feature, but an empty #Content worked for me:
interface MyControllerOpenApiSpec {
#ApiResponse(responseCode = "200") // shows MyDTO schema
#ApiResponse(responseCode = "404", content = #Content) // no schema shown
MyDTO getMyDTO();
}
There is not any content method; maybe, it is changed.
public #interface ApiResponse {
int code();
String message();
Class<?> response() default Void.class;
String reference() default "";
ResponseHeader[] responseHeaders() default {#ResponseHeader(
name = "",
response = Void.class
)};
String responseContainer() default "";
Example examples() default #Example({#ExampleProperty(
value = "",
mediaType = ""
)});
}

swagger-ui duplicating endpoints with interface and implementation of controller

I have a spring boot application. I have chosen to implement my controllers as an interface defining the endpoint and its respective implementation (i.e EndpointX, EndpointXController w/ EndpointXController being the implementation). I have all of my annotations for swagger in the interface files to prevent cluttering of the implementation class; however, I see duplicate endpoints on swagger UI as shown below:
This is my docket setup:
#Bean
public Docket customImplementation() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.withMethodAnnotation(RequestMapping.class))
.paths(PathSelectors.ant("/consent/*"))
.build()
.directModelSubstitute(java.time.LocalDate.class, java.sql.Date.class)
.directModelSubstitute(java.time.OffsetDateTime.class, java.util.Date.class)
.apiInfo(apiInfo());
}
How can I tell swagger/swagger-ui to only show one endpoint for the rest service? I.e consent-api-controller wouldn't be shown or picked up by swagger.
Edit: Posted Controller and interface code
#Controller
public class ConsentApiController implements ConsentApi {
#Autowired
private IConsentApiService consentApiService;
private final ObjectMapper objectMapper;
private final HttpServletRequest request;
#Autowired
public ConsentApiController(ObjectMapper objectMapper, HttpServletRequest request) {
this.objectMapper = objectMapper;
this.request = request;
}
#Override
public Optional<ObjectMapper> getObjectMapper() {
return Optional.ofNullable(objectMapper);
}
#Override
public Optional<HttpServletRequest> getRequest() {
return Optional.ofNullable(request);
}
public ResponseEntity getConsent(#ApiParam(value = "Identifier for the consent object to be retrieved", required = true) #Valid #RequestBody ConsentReadRequestParent consentReadRequestParent) {
return consentApiService.getConsent(consentReadRequestParent);
}
public ResponseEntity postConsent(#ApiParam(value = "Populated consent object") #Valid #RequestBody ConsentParentRequest consentObj) {
// Pass request to service where it will be split into DTOs and passed to DAOs and get response
return consentApiService.postConsent(consentObj);
}
public ResponseEntity searchConsent(#Valid #RequestBody SearchParentRequest spr){
return consentApiService.searchConsent(spr);
}
}
#Api(value = "consent")
public interface ConsentApi {
default Optional<ObjectMapper> getObjectMapper() {
return Optional.empty();
}
default Optional<HttpServletRequest> getRequest() {
return Optional.empty();
}
default Optional<String> getAcceptHeader() {
return getRequest().map(r -> r.getHeader("Accept"));
}
#ApiOperation(value = "The Read Consent API is a resource that conforms to a RESTful syntax to retrieve the details of a single consent record.", nickname = "getConsent", notes = "Cannot read without parameters. Minimum of 2 characters in each field. Maximum of 50 characters in each field. Should be able to handle special characters.", response = Consent.class, tags = {"consent",})
#ApiResponses(value = {
#ApiResponse(code = 200, message = "OK", response = Consent.class),
#ApiResponse(code = 400, message = "Bad Request"),
#ApiResponse(code = 405, message = "Method Not Allowed"),
#ApiResponse(code = 500, message = "Internal Server Error"),
#ApiResponse(code = 604, message = "Could Not Retrieve Data for Consent"),
#ApiResponse(code = 714, message = "Consent Not Found Matching Input Values")})
#RequestMapping(value = "/consent/read",
method = RequestMethod.POST,
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {MediaType.APPLICATION_JSON_VALUE})
ResponseEntity<?> getConsent(#ApiParam(value = "Identifier for the consent object to be retrieved.", required = true) #Valid #RequestBody ConsentReadRequestParent consentReadRequestParent);
#ApiOperation(value = "The Create Consent API is a resource that conforms to a RESTful syntax to persist a single consent record.", nickname = "postConsent", notes = "<business and backend logic/requirements>", response = ConsentResponseParent.class, tags = {"consent",})
#ApiResponses(value = {
#ApiResponse(code = 200, message = "OK", response = ConsentResponseParent.class),
#ApiResponse(code = 400, message = "Bad Request"),
#ApiResponse(code = 405, message = "Method Not Allowed"),
#ApiResponse(code = 500, message = "Internal Server Error")})
#RequestMapping(value = "/consent/create",
method = RequestMethod.POST,
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {MediaType.APPLICATION_JSON_VALUE})
ResponseEntity<?> postConsent(#ApiParam(value = "filled-out consent object") #Valid #RequestBody ConsentParentRequest consentObj);
#ApiOperation(value = "The Search Consent API is a resource that conforms to a RESTful syntax to query consent records.", response = SearchParentResponse.class, tags = {"consent",})
#ApiResponses(value = {
#ApiResponse(code = 200, message = "OK", response = SearchParentResponse.class),
#ApiResponse(code = 400, message = "Bad Request"),
#ApiResponse(code = 405, message = "Method Not Allowed"),
#ApiResponse(code = 500, message = "Internal Server Error")})
#RequestMapping(value = "/consent/search",
method = RequestMethod.POST,
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {MediaType.APPLICATION_JSON_VALUE})
ResponseEntity<?> searchConsent(#Valid #RequestBody SearchParentRequest spr);
}
Remove attribute "tags" from #ApiOperation annotation. Adding tags will make the api appear under the context menu as well as under its own separate menu.
Another approach is jus the #Api for the controller class:
#Controller
#Api(tags = "consent")
public class ConsentApiController implements ConsentApi {
Or into the interface:
#Api(value = "consent", tags = "consent")
public interface ConsentApi {
You could explicitly exclude the controllers that should not be shown by moving the interfaces to a different package and using
.apis(RequestHandlerSelectors.basePackage("my.impl.package"))
or by writing your own, custom set of predicates and passing them into .apis().
However this would me more like a workaround. Are you certain there are no #RequestMapping annotations used in the implementing classes? I could not replicate this behaviour locally.

Mass Assignment: Insecure Binder Configuration : How to use Spring Framework's #initBinder with Jersey framework

I want to avoid Mass Assignment: Insecure Binder Configuration issue for our application which is written in Jersey framework. I was thinking is there any other way we can use #InitBinder from spring and for each request to this service only allow to set the allowed properties and set all other properties to null.
#Controller
#Path("/ar")
#Api(tags = { "Request" })
public class RequestService extends AbstractService {
static final Logger logger = Logger
.getLogger("RequestServiceLogger");
#InitBinder
public void customizeBinding (WebDataBinder binder) {
System.out.println("Inside init binder ============== ");
//I want to allow the allowed field only for AccountRequest object
binder.setAllowedFields(allowedFields);
}
#Path("/submitrequest")
#POST
#Consumes({ "application/json" })
#Produces({ "application/json" })
#ApiOperation(value = "Validates a request", notes = "Validates a request", response = RequestResponse.class)
#ApiImplicitParams({ #io.swagger.annotations.ApiImplicitParam(name = "Auth", value = "value", required = true, dataType = "string", paramType = "header") })
#ApiResponses({
#io.swagger.annotations.ApiResponse(code = 200, message = "OK", responseHeaders = { #io.swagger.annotations.ResponseHeader(name = "X-ResponseTime", description = "Total Time Taken", response = String.class) }, response = RequestResponse.class),
#io.swagger.annotations.ApiResponse(code = 400, message = "Bad Request", response = com.model.ErrorDetail.class),
#io.swagger.annotations.ApiResponse(code = 401, message = "Unauthorized", response = com.model.ErrorDetail.class),
#io.swagger.annotations.ApiResponse(code = 403, message = "Forbidden", response = com.model.ErrorDetail.class),
#io.swagger.annotations.ApiResponse(code = 404, message = "Not Found", response = com.model.ErrorDetail.class),
#io.swagger.annotations.ApiResponse(code = 405, message = "Method Not Allowed", response = com.model.ErrorDetail.class),
#io.swagger.annotations.ApiResponse(code = 415, message = "Unsupported Media Type", response = com.model.ErrorDetail.class),
#io.swagger.annotations.ApiResponse(code = 500, message = "Internal Server error", response = com.ErrorDetail.class) })
public Response submitRequest(#ApiParam(value = "AccountRequest JSON input data.", required = true) AccountRequest accountRequest,
#Context HttpServletRequest request) throws Exception {
System.out.println("Inside submitRequest ============== ");
}
}
*** If there are any other alternative ways to filter request object properties please let me know.

Categories