Swagger-ui is not showing control documentation - kotlin

I am trying to use springfox-swagger-ui for the documentation of my rest services. I had used the next basic configuration in a kotlin project:
This is my Docket class:
#Configuration
#EnableSwagger2
open class SwaggerConfig {
#Bean
open fun newsApi(): Docket {
return Docket(DocumentationType.SWAGGER_2)
.groupName("api-infos")
.apiInfo(apiInfo())
.directModelSubstitute(LocalDateTime::class.java, Date::class.java)
.select()
.paths(regex("/api.*"))
.build()
}
private fun apiInfo(): ApiInfo {
return ApiInfoBuilder()
.title("Infos REST api")
.description("Swagger test for Api ESPN")
.termsOfServiceUrl("http://en.wikipedia.org/wiki/Terms_of_service")
.contact("rodolfo.silva#globant.com")
.license("Apache License Version 2.0")
.licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
.version("1.0")
.build()
}
}
And this is my controller:
#Controller
#ProductApi(
id = "v1_browse_player",
title = "Browse Player (v1)",
description = "")
#Api(value = "controller", description = "Controllers API", produces = "application/json")
#RequestMapping("/api/infos")
class BrowsePlayerController {
#Autowired
lateinit var browsePlayerService: BrowsePlayerServiceRepresentable
#GetRequest(
path = "/v1/browse/players",
timeToLive = 300,
queries = [
QueryParameter(name = "swid", required = true),
QueryParameter(name = "uid"),
QueryParameter(name = "seeAll", type = java.lang.Boolean::class),
QueryParameter(name = "lang", required = true),
QueryParameter(name = "region", required = true),
QueryParameter(name = "version", required = true, type = Integer::class),
QueryParameter(name = "appName", required = true),
QueryParameter(name = "platform", required = true)
]
)
#ApiOperation(value = "Get the players", notes = "Returns one info for playerBrowse")
fun processBrowsePlayerRequest(transaction: Transaction, apiRequest: ApiRequest): Single<BrowsePlayerResponse?> {
val applicationContext = RequestBasedApplicationContext(apiRequest)
val standardContext = RequestBasedStandardContext(
RequestBasedVersionContext(apiRequest),
applicationContext,
RequestBasedEditionContext(apiRequest, applicationContext),
RequestBasedPlatformContext(apiRequest),
transaction
)
val swidContext = RequestBasedSWIDContext(apiRequest)
val uidContext = if (checkUIDPresent(apiRequest)) RequestBasedUIDContext(apiRequest) else null
val seeAllContext = RequestBasedSeeAllContext(apiRequest)
val requestBrowsePlayerContext = RequestBrowsePlayerContext(standardContext, swidContext, uidContext, seeAllContext, apiRequest)
return browsePlayerService.getEntitiesBrowse(requestBrowsePlayerContext)
}
private fun checkUIDPresent(apiRequest: ApiRequest): Boolean =
apiRequest.parameters["uid"] != null
}
I had used a very basic configuration, the ApiOperation, Api and RequestMapping("/api/infos") tags, also at the data class level, the next configuration:
#JsonInclude(JsonInclude.Include.NON_NULL)
data class TopBrowsePlayerHeader(val title: String, val searchURL: String?)
#ApiModel(value = "Info entity", description = "Entity class BrowsePlayerResponse")
data class BrowsePlayerResponse(
#ApiModelProperty(value = "The header of the info", required = false)
val header: TopBrowsePlayerHeader,
#ApiModelProperty(value = "The analytics node of the info", required = true)
val analytics: Analytics,
#ApiModelProperty(value = "The sections node of the info", required = true)
val sections: List<Section>)
When I load the http://localhost:8080/swagger-ui.html#/api-controller (swagger browser). I can't see my controller structure. It seems like there is a predefined endpoint which is shown:
http://localhost:8080/v2/api-docs?group=api-infos
I am not pretty familiar with this configuration. Any ideas to the correct configuration?
Thanks

Try to replace paths value with PathSelectors.any():
#Bean
open fun newsApi() : Docket {
return Docket(DocumentationType.SWAGGER_2)
.groupName("api-infos")
.apiInfo(apiInfo())
.directModelSubstitute(LocalDateTime::class.java, Date::class.java)
.select()
.paths(PathSelectors.any())
.build()
}
The default value for swagger path is /v2/api-docs.
You can change is in the application.properties with the springfox.documentation.swagger.v2.path key to whatever you want to.
?group=api-infos comes from the .groupName("api-infos") value.
If you don't want to group your APIs for any reasons (like sets of selected APIs for specific clients) then remove the .groupName(...).

Related

picocli - parse file automatically when specified

I have a CLI with multiple sub-commands, some of the sub-commands have an optional flag -f with which an input file can be specified, e.g.
#CommandLine.Command(name = "get", description = ["Get something"])
class GetUserCommand: Runnable {
#Option(names = ["-f", "--file"], description = ["Input file"])
var filename: String? = null
override fun run() {
var content = read_file(filename)
}
}
#CommandLine.Command(name = "query", description = ["Query something"])
class QueryUserCommand: Runnable {
#Option(names = ["-f", "--file"], description = ["Input file"])
var filename: String? = null
override fun run() {
var content = read_file(filename)
}
}
The input file format can be different from command to command. Ideally, I'd like to parse the file automatically if it was specified as an argument.
Also the file content can be different on each command (but will be a specific format, CSV or JSON).
For example I'd like to have something like this
data class First(val col1, val col2)
data class Second(val col1, val col2, val col3)
class CustomOption(// regular #Option parameters, targetClass=...) {
// do generic file parsing
}
#CommandLine.Command(name = "get", description = ["Get something"])
class GetUserCommand: Runnable {
#CustomOption(names = ["-f", "--file"], description = ["Input file"], targetClass=First))
var content: List<First> = emptyList()
override fun run() {
// content now contains the parse file
}
}
#CommandLine.Command(name = "query", description = ["Query something"])
class QueryUserCommand: Runnable {
#CustomOption(names = ["-f", "--file"], description = ["Input file"], targetClass=Second))
var content: List<Second> = emptyList()
override fun run() {
// content now contains the parse file
}
}
Would anyone have an idea if this is possible or how to do it?
To rephrase the question: how to do additional processing of input parameters during the parsing process rather than during the command execution?
(Note that the OP did not specify why this is desirable. I assume the goal is either to leverage picocli's error reporting, or to encapsulate the parsing logic somewhere for easier testing and reuse. The OP may want to expand on the underlying goal if the solution below is not satisfactory.)
One idea is to use picocli's custom parameter processing.
It is possible to specify a IParameterConsumer for an option that will process the parameter for that option.
So, for example when the user specifies get -f somefile, the custom parameter consumer will be responsible for processing the somefile argument. An implementation can look something like this:
// java implementation, sorry I am not that fluent in Kotlin...
class FirstConsumer implements IParameterConsumer {
public void consumeParameters(Stack<String> args,
ArgSpec argSpec,
CommandSpec commandSpec) {
if (args.isEmpty()) {
throw new ParameterException(commandSpec.commandLine(),
"Missing required filename for option " +
((OptionSpec) argSpec).longestName());
}
String arg = args.pop();
First first = parseFile(new File(arg), commandSpec);
List<String> list = argSpec.getValue();
list.add(first);
}
private First parseFile(File file,
ArgSpec argSpec,
CommandSpec commandSpec) {
if (!file.isReadable()) {
throw new ParameterException(commandSpec.commandLine(),
"Cannot find or read file " + file + " for option " +
((OptionSpec) argSpec).longestName());
}
// other validation...
// parse file contents...
// finally, return the result...
return new First(...);
}
}
Once the parameter consumer classes are defined, you can use them as follows:
#Command(name = "get", description = ["Get something"])
class GetUserCommand: Runnable {
#Option(names = ["-f", "--file"], description = ["Input file"],
parameterConsumer = FirstConsumer::class))
var content: List<First> = emptyList()
override fun run() {
// content now contains the parsed file
}
}

Parse map with dynamic keys with Klaxon

I have the following JSON snippet:
{
"hd":{
"hdEnabled":true,
"defaultStreamQualitySetting":"HD720",
"streamQualitySettings":{
"SD":"SD - low quality",
"HD720":"Standard HD - 720p",
"HD1080":"Full HD - 1080p"
}
}
}
I want to parse the streamQualitySettings with Klaxon and Gson to an object called 'Option' that has a key & description so that I end of with a list of 3 options
Option(key = SD, description = "SD - low quality")
Option(key = HD720, description = "Standard HD - 720p")
Option(key = HD1080, description = "Full HD - 1080p")
How can I achieve this with Klaxon (or Gson)?
This is my code
val jsonArray = bootstrapJsonObject()
.lookup<JsonArray<JsonObject>>("hd.streamQualitySettings")
.first()
val gson = Gson()
val options = ArrayList<Option>()
jsonArray.forEach {
options.add(gson.fromJson(it.toJsonString(), Option::class.java))
}
Why are you using both gson and klaxon? If you want to use gson, then kotson is an alternative with a fluent kotlin dsl.
Here is a solution with klaxon:
fun convert(input: String): List<Option> {
val streamObj = (Parser.default().parse(StringBuilder(input)) as JsonObject)
.obj("hd")!!
.obj("streamQualitySettings")!!
return streamObj.keys.map { Option(it, streamObj.string(it)!!) }
}
Parse, then move down to the streamQualitySettings.
Get all the keys and map them to Option.

I am using TestRestTemplate to Test with #RequestParam value how to execute

//how to send #RequestParam value to url
enter code here#ApiRestController
public class CityController extends BaseController{
#GetMapping("/cities")
public ResponseEntity<CitiesResponse> getAll(
#RequestParam(value = "pageNumber", defaultValue = "1") int pageNumber,
#RequestParam(value = "pageSize", defaultValue = "100") int pageSize,
#RequestParam(value = "sortBy", defaultValue = "id", required = false) String sortBy,
#RequestParam(value = "sortDirection", defaultValue = "asc", required = false) String sortDirection,
#RequestParam(value = "search", required = false) String search) {
return new ResponseEntity(cityService.getAll(pageNumber, pageSize, sortBy, sortDirection, search), HttpStatus.OK);
}
}
To easily manipulate URLs / path / params / etc., you can use Spring's UriComponentsBuilder class. It's cleaner that manually concatenating strings and it takes care of the URL encoding for you:
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url)
.queryParam("pageNumber", 1)
.queryParam("pageSize", 10)
.queryParam("sortBy", "id")
.queryParam("sortDirection", "desc")
.queryParam("search", "hello search");
HttpEntity<?> entity = new HttpEntity<>(headers); //Update this as per your code
HttpEntity<String> response = restTemplate.exchange(
builder.build().encode().toUri(),
HttpMethod.GET,
entity,
String.class);
There are different ways to test in spring boot. Check the samples below:
First option:
It's more like an integration test. In this case the port will be the default 8080
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class DemoApplicationTests {
private TestRestTemplate restTemplate = new TestRestTemplate();
#Test
public void contextLoads() {
String url = "http://localhost:8080";
URI uri = UriComponentsBuilder.fromHttpUrl(url).path("/books")
.queryParam("order", "asc").build().toUri();
this.restTemplate.getForEntity(uri, Void.class);
}
}
Second option:
Very similar to the first option but this time it will run in an random port which can be capture by #LocalServerPort
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class DemoApplicationTests {
#LocalServerPort
private int port;
private TestRestTemplate restTemplate = new TestRestTemplate();
#Test
public void contextLoads() {
String url = "http://localhost:" + this.port;
URI uri = UriComponentsBuilder.fromHttpUrl(url).path("/books")
.queryParam("order", "asc").build().toUri();
this.restTemplate.getForEntity(uri, Void.class);
}
}
UriComponentsBuilder has been used to build the uri in a very friendly way.
Third option:
This option doesn't involve TestRestTemplate but just involve the RestController by itself. Any dependency inside the controller should be mark with #MockBean in the test.
#RunWith(SpringRunner.class)
#WebMvcTest(BookRestController.class)
public class DemoApplicationTests {
#Autowired
private MockMvc mvc;
#Test
public void contextLoads() throws Exception {
this.mvc.perform(MockMvcRequestBuilders.get("/books")
.param("order", "asc"))
.andExpect(MockMvcResultMatchers.status().isOk());
}
}

Swashbuckle 5 and multipart/form-data HelpPages

I am stuck trying to get Swashbuckle 5 to generate complete help pages for an ApiController with a Post request using multipart/form-data parameters. The help page for the action comes up in the browser, but there is not included information on the parameters passed in the form. I have created an operation filter and enabled it in SwaggerConfig, the web page that includes the URI parameters, return type and other info derived from XML comments shows in the browser help pages; however, nothing specified in the operation filter about the parameters is there, and the help page contains no information about the parameters.
I must be missing something. Are there any suggestion on what I may have missed?
Operation filter code:
public class AddFormDataUploadParamTypes : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription) {
if (operation.operationId == "Documents_Upload")
{
operation.consumes.Add("multipart/form-data");
operation.parameters = new[]
{
new Parameter
{
name = "anotherid",
#in = "formData",
description = "Optional identifier associated with the document.",
required = false,
type = "string",
format = "uuid"
},
new Parameter
{
name = "documentid",
#in = "formData",
description = "The document identifier of the slot reserved for the document.",
required = false,
type = "string",
format = "uuid"
},
new Parameter
{
name = "documenttype",
#in = "formData",
description = "Specifies the kind of document being uploaded. This is not a file name extension.",
required = true,
type = "string"
},
new Parameter
{
name = "emailfrom",
#in = "formData",
description = "A optional email origination address used in association with the document if it is emailed to a receiver.",
required = false,
type = "string"
},
new Parameter
{
name = "emailsubject",
#in = "formData",
description = "An optional email subject line used in association with the document if it is emailed to a receiver.",
required = false,
type = "string"
},
new Parameter
{
name = "file",
#in = "formData",
description = "File to upload.",
required = true,
type = "file"
}
};
}
}
}
With Swashbuckle v5.0.0-rc4 methods listed above do not work. But by reading OpenApi spec I have managed to implement a working solution for uploading a single file. Other parameters can be easily added:
public class FileUploadOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var isFileUploadOperation =
context.MethodInfo.CustomAttributes.Any(a => a.AttributeType == typeof(YourMarkerAttribute));
if (!isFileUploadOperation) return;
var uploadFileMediaType = new OpenApiMediaType()
{
Schema = new OpenApiSchema()
{
Type = "object",
Properties =
{
["uploadedFile"] = new OpenApiSchema()
{
Description = "Upload File",
Type = "file",
Format = "binary"
}
},
Required = new HashSet<string>()
{
"uploadedFile"
}
}
};
operation.RequestBody = new OpenApiRequestBody
{
Content =
{
["multipart/form-data"] = uploadFileMediaType
}
};
}
}
I presume you figured out what your problem was. I was able to use your posted code to make a perfect looking 'swagger ui' interface complete with the file [BROWSE...] input controls.
I only modified your code slightly so it is applied when it detects my preferred ValidateMimeMultipartContentFilter attribute stolen from Damien Bond. Thus, my slightly modified version of your class looks like this:
public class AddFormDataUploadParamTypes<T> : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
var actFilters = apiDescription.ActionDescriptor.GetFilterPipeline();
var supportsDesiredFilter = actFilters.Select(f => f.Instance).OfType<T>().Any();
if (supportsDesiredFilter)
{
operation.consumes.Add("multipart/form-data");
operation.parameters = new[]
{
//other parameters omitted for brevity
new Parameter
{
name = "file",
#in = "formData",
description = "File to upload.",
required = true,
type = "file"
}
};
}
}
}
Here's my Swagger UI:
FWIW:
My NuGets
<package id="Swashbuckle" version="5.5.3" targetFramework="net461" />
<package id="Swashbuckle.Core" version="5.5.3" targetFramework="net461" />
Swagger Config Example
public class SwaggerConfig
{
public static void Register()
{
var thisAssembly = typeof(SwaggerConfig).Assembly;
GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
c.Schemes(new[] { "https" });
// Use "SingleApiVersion" to describe a single version API. Swagger 2.0 includes an "Info" object to
// hold additional metadata for an API. Version and title are required but you can also provide
// additional fields by chaining methods off SingleApiVersion.
//
c.SingleApiVersion("v1", "MyCorp.WebApi.Tsl");
c.OperationFilter<MyCorp.Swashbuckle.AddFormDataUploadParamTypes<MyCorp.Attr.ValidateMimeMultipartContentFilter>>();
})
.EnableSwaggerUi(c =>
{
// If your API supports ApiKey, you can override the default values.
// "apiKeyIn" can either be "query" or "header"
//
//c.EnableApiKeySupport("apiKey", "header");
});
}
}
UPDATE March 2019
I don't have quick access to the original project above, but, here's an example API controller from a different project...
Controller signature:
[ValidateMimeMultipartContentFilter]
[SwaggerResponse(HttpStatusCode.OK, Description = "Returns JSON object filled with descriptive data about the image.")]
[SwaggerResponse(HttpStatusCode.NotFound, Description = "No appropriate equipment record found for this endpoint")]
[SwaggerResponse(HttpStatusCode.BadRequest, Description = "This request was fulfilled previously")]
public async Task<IHttpActionResult> PostSignatureImage(Guid key)
You'll note that there's no actual parameter representing my file in the signature, you can see below that I just spin up a MultipartFormDataStreamProvider to suck out the incoming POST'd form data.
Controller Body:
var signatureImage = await db.SignatureImages.Where(img => img.Id == key).FirstOrDefaultAsync();
if (signatureImage == null)
{
return NotFound();
}
if (!signatureImage.IsOpenForCapture)
{
ModelState.AddModelError("CaptureDateTime", $"This equipment has already been signed once on {signatureImage.CaptureDateTime}");
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
string fileName = String.Empty;
string ServerUploadFolder = System.Web.Hosting.HostingEnvironment.MapPath("~/App_Data/");
DirectoryInfo di = new DirectoryInfo(ServerUploadFolder + key.ToString());
if (di.Exists == true)
ModelState.AddModelError("id", "It appears an upload for this item is either in progress or has already occurred.");
else
di.Create();
var fullPathToFinalFile = String.Empty;
var streamProvider = new MultipartFormDataStreamProvider(di.FullName);
await Request.Content.ReadAsMultipartAsync(streamProvider);
foreach (MultipartFileData fileData in streamProvider.FileData)
{
if (string.IsNullOrEmpty(fileData.Headers.ContentDisposition.FileName))
{
return StatusCode(HttpStatusCode.NotAcceptable);
}
fileName = cleanFileName(fileData.Headers.ContentDisposition.FileName);
fullPathToFinalFile = Path.Combine(di.FullName, fileName);
File.Move(fileData.LocalFileName, fullPathToFinalFile);
signatureImage.Image = File.ReadAllBytes(fullPathToFinalFile);
break;
}
signatureImage.FileName = streamProvider.FileData.Select(entry => cleanFileName(entry.Headers.ContentDisposition.FileName)).First();
signatureImage.FileLength = signatureImage.Image.LongLength;
signatureImage.IsOpenForCapture = false;
signatureImage.CaptureDateTime = DateTimeOffset.Now;
signatureImage.MimeType = streamProvider.FileData.Select(entry => entry.Headers.ContentType.MediaType).First();
db.Entry(signatureImage).State = EntityState.Modified;
try
{
await db.SaveChangesAsync();
//cleanup...
File.Delete(fullPathToFinalFile);
di.Delete();
}
catch (DbUpdateConcurrencyException)
{
if (!SignatureImageExists(key))
{
return NotFound();
}
else
{
throw;
}
}
char[] placeHolderImg = paperClipIcon_svg.ToCharArray();
signatureImage.Image = Convert.FromBase64CharArray(placeHolderImg, 0, placeHolderImg.Length);
return Ok(signatureImage);
Extending #bkwdesign very useful answer...
His/her code includes:
//other parameters omitted for brevity
You can actually pull all the parameter information (for the non-multi-part form parameters) from the parameters to the filter. Inside the check for supportsDesiredFilter, do the following:
if (operation.parameters.Count != apiDescription.ParameterDescriptions.Count)
{
throw new ApplicationException("Inconsistencies in parameters count");
}
operation.consumes.Add("multipart/form-data");
var parametersList = new List<Parameter>(apiDescription.ParameterDescriptions.Count + 1);
for (var i = 0; i < apiDescription.ParameterDescriptions.Count; ++i)
{
var schema = schemaRegistry.GetOrRegister(apiDescription.ParameterDescriptions[i].ParameterDescriptor.ParameterType);
parametersList.Add(new Parameter
{
name = apiDescription.ParameterDescriptions[i].Name,
#in = operation.parameters[i].#in,
description = operation.parameters[i].description,
required = !apiDescription.ParameterDescriptions[i].ParameterDescriptor.IsOptional,
type = apiDescription.ParameterDescriptions[i].ParameterDescriptor.ParameterType.FullName,
schema = schema,
});
}
parametersList.Add(new Parameter
{
name = "fileToUpload",
#in = "formData",
description = "File to upload.",
required = true,
type = "file"
});
operation.parameters = parametersList;
first it checks to make sure that the two arrays being passed in are consistent. Then it walks through the arrays to pull out the required info to put into the collection of Swashbuckle Parameters.
The hardest thing was to figure out that the types needed to be registered in the "schema" in order to have them show up in the Swagger UI. But, this works for me.
Everything else I did was consistent with #bkwdesign's post.

Swagger responseContainer = "List" not working

Below is my interface
#Api(value = "/abc/def", basePath = "/abc/def", description = "")
public interface test{
#ApiOperation(value = "some description" , response = SomeDTO.class ,responseContainer = "List" )
#ApiResponses(value = {
#ApiResponse(code = 200, message = "Successful response"),
})
#GET
#Path("/{Id}")
List<SomeDTO> getById(#PathParam("Id") String Id);
}
In the Swagger UI when i provide the Id and Click on Try out it doesn't display the list of results . It just hangs.
Below are my Dependencies
<dependency>
<groupId>com.wordnik</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.3.10</version>
</dependency>
SomeDTO Looks like below in Swagger UI
SomeDTO {
Id (integer, optional),
TestType (TestType, optional) = ['QA' or 'PROD'], // enum in java class
Time (string, optional),
Status (Status, optional) = ['Y' or 'N'], // enum in java class
}
SomeDTO.java
public class SomeDTO {
private Integer Id;
private TestType testType;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy HH:mm:ss")
private Date Time;
private Status status;
// getters and setters
}
I am able to get the results by Chrome Poster Plugin .
Can someone please let me know what i have missed while configuring swagger to return a list of SomeDTO types..