I have a couple of APIs and using springfox-swagger for API documentation.
I have a requirement to add the creation date to the respective API. How can I achieve this using swagger. I don't need any API versioning.
Ex:
#ApiOperation(value = "Creates a new user and returns the created user.")
#PostMapping(/user)
public ResponseEntity<UserDto> createUser(#RequestBody UserDto userDto) {
User user =userService.create(userDto);
return new ResponseEntity<>(UserMappers.USER_ENTITY_TO_DTO.apply(user),HttpStatus.CREATED);
}
In the above example, I want to add the creation date of /user so that I can trace the creation date.
In my project I have a similar requirement. As a solution I have created a custom annotation (for marking the endpoint) and wrote a plugin (for updating the API description).
Option #1
#ApiSince annotation:
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
public #interface ApiSince {
String value() default "";
}
ApiSincePlugin plugin:
#Component
public class ApiSincePlugin implements OperationBuilderPlugin {
private final DescriptionResolver resolver;
#Autowired
public ApiSincePlugin(DescriptionResolver resolver) {
this.resolver = resolver;
}
#Override
public void apply(OperationContext context) {
final String sinceTemplate = "### Since %s%n%n%s";
String notes = "";
Optional<ApiOperation> apiOperationOptional = context.findAnnotation(ApiOperation.class);
if (apiOperationOptional.isPresent()) {
notes = apiOperationOptional.get().notes();
}
String finalNotes = notes;
Optional<ApiSince> apiSinceOptional = context.findAnnotation(ApiSince.class);
if (apiSinceOptional.isPresent()) {
finalNotes = String.format(sinceTemplate, apiSinceOptional.get().value(), notes);
}
context.operationBuilder().notes(resolver.resolve(finalNotes));
}
#Override
public boolean supports(DocumentationType type) {
return true;
}
}
#ApiSince in action:
#ApiSince(value = "2019-10-31")
#PostMapping(value = "/login")
#ApiOperation(value = "Authenticate user", nickname = "login", notes = "your API description")
#ResponseStatus(HttpStatus.OK)
#ApiResponses(value = {
#ApiResponse(code = 200, response = LoginResponse.class, message = HTTP_200_OK),
...
})
#ResponseBody
ResponseEntity<LoginResponse> login(...);
If you don't want do add it in the description but as an extra JSON attribute then take a look at this solution: Custom Operation Builder Plugin
.
Option #2
#ApiSince annotation (code same as above)
ApiSincePlugin plugin:
#Component
public class ApiSincePlugin implements OperationBuilderPlugin {
#Override
public void apply(OperationContext context) {
Optional<ApiSince> annotation = context.findAnnotation(ApiSince.class);
if (annotation.isPresent()) {
String value = annotation.get().value();
ObjectVendorExtension extention = new ObjectVendorExtension("x-since");
extention.addProperty(new StringVendorExtension("value", value));
context.operationBuilder().extensions(Collections.singletonList(extention));
}
}
#Override
public boolean supports(DocumentationType documentationType) {
return true;
}
}
Activate extensions in the Swagger UI:
#Bean
UiConfiguration uiConfig() {
return UiConfigurationBuilder
.builder()
.showExtensions(true)
...
.build();
}
#ApiSince in action (code same as above):
Related
I'm using springdoc-openapi-ui 1.6.14
I have following class
#Configuration
public class GroupsConfig {
private final PropertyResolver propertyResolver;
public GroupsConfig(PropertyResolver propertyResolver) {
this.propertyResolver = propertyResolver;
}
#Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/admin/**")
.build();
}
#Bean
public GroupedOpenApi externalApi() {
return GroupedOpenApi.builder()
.group("external")
.pathsToMatch("/external/**")
.build();
}
#Bean
public GroupedOpenApi clientApi() {
return GroupedOpenApi.builder()
.group("client")
.pathsToMatch("/client/**")
.build();
}
#Bean
public GroupedOpenApi externalClientApi() {
return GroupedOpenApi.builder()
.group("extclient")
.pathsToMatch("/extclient/**")
.build();
}
#Bean
public OpenAPI apiInfo() {
String title = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_TITLE);
String description = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_DESCRIPTION);
String version = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_VERSION);
String contactName = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_CONTACT_NAME);
String contactUrl = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_CONTACT_URL);
String contactEmail = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_CONTACT_EMAIL);
String termsOfServiceUrl = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_TERMS_OF_SERVICE_URL);
String licence = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_LICENCE);
String licenceUrl = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_LICENCE_URL);
Contact contact = new Contact()
.name(contactName)
.url(contactUrl)
.email(contactEmail);
return new OpenAPI()
.info(new Info().title(title)
.description(description)
.version(version)
.license(new License().name(licence).url(licenceUrl))
.contact(contact)
.termsOfService(termsOfServiceUrl))
.components(new Components());
}
}
The OpenAPI info is working correctly and displayed in the UI.
I then have follewing class to import all Springdoc configurations manually
#Configuration
#Import({org.springdoc.core.SpringDocConfigProperties.class,
org.springdoc.webmvc.core.MultipleOpenApiSupportConfiguration.class,
org.springdoc.core.SpringDocConfiguration.class, org.springdoc.webmvc.core.SpringDocWebMvcConfiguration.class,
SwaggerUiConfigParameters.class, SwaggerUiOAuthProperties.class,
org.springdoc.core.SwaggerUiConfigProperties.class, org.springdoc.core.SwaggerUiOAuthProperties.class,
org.springdoc.webmvc.ui.SwaggerConfig.class, GroupsConfig.class,
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.class})
public class SwaggerConfig {
}
If I go to /v3/api-docs, I get a giant JSON with all the different paths in my application. If I go to /v3/api-docs/admin, I get a 404. So the GroupedOpenApi beans are not getting picked up by Springdoc.
Anyone having the same issue or an idea how to fix this?
Thanks in advance!
Edit: I just tried it with 1.4.4 and it works. What should I do to get it working with the newest version?
So far I am creating end points like this:
#RequestMapping(value = "/test", method = RequestMethod.POST)
public #ResponseBody String indexPost(HttpServletRequest request, HttpServletResponse response)
throws Exception {
//Doing calculations
return "Result";
}
But I would like to reach the application.properties when the server starts, to read out the data stored like this:
methods: {
"endpointOne": "DBStoredProcedure1",
"endpointTwo": "DBStoredProcedure2"
}
So when my Spring Boot application starts, it would create all the POST endpoints based on the property file with the names of the first parameters (like "endpointOne"), and would call (and return the result of) the stored procedure which is defined as the second parameter (like "DBStoredProcedure1").
Is it possible to do?
Yes you can. A little bit differently though than you try to do it at the moment.
The best is to use a "PathVariable" - you find detailed information here:
https://spring.io/guides/tutorials/bookmarks/
http://javabeat.net/spring-mvc-requestparam-pathvariable/
Your method at the Controller class would look something like this:
#RequestMapping(value="/{endPoint}", method=RequestMethod.POST)
public String endPoint(#PathVariable String endPoint) {
//Your endPoint is now the one what the user would like to reach
//So you check if your property file contains this String - better to cache it's content
//If it does, you call the service with the regarding Stored Procedure.
String sPName = prop.getSPName(endPoint); //You need to implement it.
String answer = yourService.execute(sPName);
return answer;
}
Obviously you need to implement a method to handle those queries which are not found in the property file, but you get the idea.
You can use a wild card "/*" as the value in controller. So that all your endpoints would hit the same controller request method.
Below is the code sample.
#RequestMapping(value = "/*", method = RequestMethod.GET, headers="Accept=*/*", produces = { "application/json" })
public ResponseEntity<Object> getData(#RequestParam Map<String, String> reqParam, HttpServletRequest request)
throws WriteException, IOException{
MessageLogger.infoLog(EquityControllerImpl.class, GETQADTRSTOCKPRICELOGS,
ENTRY);
// Get Request URI
MessageLogger.infoLog("Request URI: " + request.getRequestURI());
String requestUri = request.getRequestURI();
//Read all request parameters
Map<String, String> requestParamMap = new HashMap<>();
for (Map.Entry<String, String> param: reqParam.entrySet()
) {
System.out.println("Parameter: " + param.getKey() + " ----> Value: " + param.getValue());
requestParamMap.put(param.getKey(),param.getValue());
}
}
Also you can define static swagger.json and use this in the swagger configuration.
#Configuration
#EnableSwagger2
#Import(SpringDataRestConfiguration.class)
public class SwaggerConfig {
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.finance.dataplatform.*"))
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build().apiInfo(getApiInfo());
}
private ApiInfo getApiInfo() {
return new ApiInfoBuilder().title("Investment Management").build();
}
private static Predicate<String> matchPathRegex(final String... pathRegexs) {
return new Predicate<String>() {
#Override
public boolean apply(String input) {
for (String pathRegex : pathRegexs) {
if (input.matches(pathRegex)) {
return true;
}
}
return false;
}
};
}
#Bean
WebMvcConfigurer configurer () {
return new WebMvcConfigurerAdapter() {
#Override
public void addResourceHandlers (ResourceHandlerRegistry registry) {
registry.addResourceHandler("/config/swagger.json")
.addResourceLocations("classpath:/config");
}
};
}
#Primary
#Bean
public SwaggerResourcesProvider swaggerResourcesProvider(InMemorySwaggerResourcesProvider defaultResourcesProvider) {
return () -> {
SwaggerResource wsResource = new SwaggerResource();
wsResource.setName("default");
wsResource.setSwaggerVersion("2.0");
wsResource.setLocation("/config/swagger.json");
//List<SwaggerResource> resources = new ArrayList<>(defaultResourcesProvider.get());
List<SwaggerResource> resources = new ArrayList<>();
resources.add(wsResource);
return resources;
};
}
}
I am new to Jersey and trying to convert a project from Spring MVC into Jersey. With my current build however, all requests return a resource not available error. Any help would be greatly appreciated.
My Dependencies:
dependencies {
compile('org.springframework.boot:spring-boot-starter')
compile("org.springframework.boot:spring-boot-starter-data-jpa:1.4.0.RELEASE")
compile("org.springframework.boot:spring-boot-starter-jersey")
runtime('org.hsqldb:hsqldb')
compileOnly("org.springframework.boot:spring-boot-starter-tomcat")
testCompile('org.springframework.boot:spring-boot-starter-test')
}
My Jersey Config
#Configuration
public class JersyConfig extends ResourceConfig {
public JersyConfig() {
registerEndpoints();
configureSwagger();
}
private void configureSwagger() {
register(ApiListingResource.class);
BeanConfig beanConfig = new BeanConfig();
beanConfig.setVersion("1.0.0");
beanConfig.setSchemes(new String[]{"http"});
beanConfig.setHost("localhost:8090");
beanConfig.setBasePath("/");
beanConfig.setResourcePackage(OwnerController.class.getPackage().getName());
beanConfig.setPrettyPrint(true);
beanConfig.setScan(true);
}
private void registerEndpoints() {
register(OwnerController.class);
}
}
#Api(value = "Owner controller", tags = {"Owner resource"})
public class OwnerController {
private final ClinicService clinicService;
#Autowired
public OwnerController(ClinicService clinicService) {
this.clinicService = clinicService;
}
#GET
#Path("/{ownerId}")
#Produces(MediaType.APPLICATION_JSON)
#ApiOperation(value = "get owner by id", response = Owner.class)
public Response getOwner(
#ApiParam(name = "owner id", value = "owner id that must be fetched") #PathParam("ownerId") int id ) {
Owner owner = clinicService.findOwnerById(id);
return Response.status(200).entity(owner).build();
}
#GET
#Path("/owners")
#Produces(MediaType.APPLICATION_JSON)
#ApiOperation(value = "get all owners", response = Owner.class, responseContainer = "List")
public Response getOwners() {
List<Owner> owner = (List<Owner>) clinicService.findAllOwners();
return Response.status(200).entity(owner).build();
}
}
Register your package which contains jersey resources using packages() method in JerseryConfig() constructor -
public JersyConfig() {
packages("PACKAGE_CONTAINING_JERSEY_RESOURCES");
registerEndpoints();
configureSwagger();
}
I am using Retrofit for post request to Spring Boot service but always is called failure Callback method. This is my simplified code:
Spring Boot service (Controller):
#RestController
public class ServController {
#Autowired
private UserRepository userRepository;
#RequestMapping(value = "/user", method = RequestMethod.POST)
public Boolean signUpUser(#RequestBody User user)
{
return true;
}
}
My client interface:
public interface ChainApi {
public static final String USER_PATH = "/user";
#POST(USER_PATH)
public void signUpUser(#Body User user, Callback<Boolean> callback);
}
Async POST request:
User user = new User();
user.setId(12);
user.setName(nameEtx.getText().toString());
user.setEmail(emailEtx.getText().toString());
user.setPassword(passwordEtx.getText().toString());
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(Constant.URL_LOCALHOST)
.build();
ChainApi service = restAdapter.create(ChainApi.class);
service.signUpUser(user, new Callback<Boolean>() {
#Override
public void success(Boolean aBoolean, Response response) {
Log.i(TAG, "Succesfull");
#Override
public void failure(RetrofitError error) {
Log.i(TAG, "Error " + error.getMessage()); // 400 Bad Request
}
});
This is my User class(POJO):
#JsonIgnoreProperties(value = { "additionalProperties"})
public class User {
#JsonProperty("id")
private Integer id;
#JsonProperty("name")
private String name;
#JsonProperty("password")
private String password;
#JsonProperty("email")
private String email;
#JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
// methods
}
NOTES: I am developing in Android and when make manual POST request from Postman I get 200 OK.
In addition I get in logcat message: 400 Bad Request
Retrofit uses GSON as its default Converter. #JsonIgnoreProperties is an annotation from Jackson. Looking at your RestAdapter you don't seem to be specifying a Jackson Converter.
Square has implemented a JasksonConverter, you use it by including the dependency.
compile 'com.squareup.retrofit:converter-jackson:X.X.X'
Use the version that matches your Retrofit version.
Then
JacksonConverter converter = JacksonConverter(new ObjectMapper());
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(Constant.URL_LOCALHOST)
.setConverter(converter)
.build();
My Question is How can I validate request parameters if I use #PathParam.
For instance I have two request parameters, name and id
path is localhost:/.../search/namevalue/idvalue
if a user submits blanks for name or id I should send a response mentioning that name is required/ id is required.
I could do the validations if I use #QueryParam, but I'm not sure how to do it if I have to use pathvariables.
If I just test using http:/localhost:/.../search/namevalue orhttp:/localhost:/.../search/idvalue or http:/localhost:/.../search/ it's throwing servlet exception.
Below is the code, if i use QueryParams validations work just fine, Please let me know the approach when i use pathparam
#Controller
#Path("/customer")
public class CustomerController extends BaseController implements Customer {
#Override
#GET
#Produces({ "application/json", "application/xml" })
#Path("/search/{name}/{id}/")
public Response searchCustomerDetails(
#PathParam("name") String name,
#PathParam("id") Integer id) {
ResponseBuilder response = null;
CustomerValidations validations = (CustomerValidations) getAppContext()
.getBean(CustomerValidations.class);
CustomerResponse customerResponse = new CustomerResponse();
CustomerService customerService = (CustomerService) getAppContext()
.getBean(CustomerService.class);
try {
validations.searchCustomerDetailsValidation(
name, id,customerResponse);
if (customerResponse.getErrors().size() == 0) {
CustomerDetails details = customerService
.searchCustomerDetailsService(name, id);
if (details == null) {
response = Response.status(Response.Status.NO_CONTENT);
} else {
customerResponse.setCustomerDetails(details);
response = Response.status(Response.Status.OK).entity(
customerResponse);
}
} else {
response = Response.status(Response.Status.BAD_REQUEST).entity(
customerResponse);
}
}
catch (Exception e) {
LOGGER.error(e.getMessage());
response = Response.status(Response.Status.INTERNAL_SERVER_ERROR);
}
return response.build();
} }
#Component
#Scope("prototype")
public class CustomerValidations {
public void searchCustomerDetailsValidation(
String name, Integer id,
CustomerResponse customerResponse) {
if (id == null) {
customerResponse.getErrors().add(
new ValidationError("BAD_REQUEST",
""invalid id));
}
if (name== null
|| (name!= null && name
.trim().length() == 0)) {
customerResponse.getErrors().add(
new ValidationError("BAD_REQUEST", "invalid id"));
}
} }
#XmlRootElement
public class CustomerResponse {
private CustomerDetails customerDetails;
private List<ValidationError> errors = new ArrayList<ValidationError>();
//setters and getters }
public class ValidationError {
private String status;
private String message;
public ValidationError() {
}
public ValidationError(String status, String message) {
super();
this.status = status;
this.message = message;
}
//setters and getters }
You're receiving an exception because you have no methods mapped to #Path("/search/{foo}/") or #Path("/search/"), so you should be getting a default 404 response as these paths are not really defined.
I'm not sure why you would want to validate these "missing" request paths though - it looks like this endpoint is intended to be used as a query endpoint so I'd suggest you use #RequestParam/query parameters to more RESTfully describe the search you're attempting. A path of search/{name}/{id} would suggest a specific resource which permanently lives at this URL, though in this case you're querying for customers on this controller.
I would propose you drop the /search path completely and just map query parameters onto the "root" of the Customer controller, so you get something like
#Controller
#Path("/customer")
public class CustomerController extends BaseController implements Customer {
#GET
#Produces({"application/json", "application/xml"})
public Response searchCustomerDetails(
#RequestParam("name") String name,
#RequestParam("id") Integer id) {
// Returns response with list of links to /customer/{id} (below)
}
#GET
#Produces({"application/json", "application/xml"})
#Path("/{id}")
public Response getCustomerDetails(#PathVariable("id") String id) {
// GET for specific Customer
}
}