Skip to content

Commit

Permalink
feat(swagger): Upgrade springfox to openapi
Browse files Browse the repository at this point in the history
This removes the outdated springfox library that was serving the swagger <3 version.
openapi introduces swagger with version >=3 using the new openapi standard. The package is maintained and compatible with newer spring-boot versions.

Co-authored-by: Amandus Butzer <amandus.butzer@heigit.org>
  • Loading branch information
MichaelsJP and TheGreatRefrigerator committed Jun 19, 2023
1 parent 9063b1b commit b765ce9
Show file tree
Hide file tree
Showing 63 changed files with 1,173 additions and 766 deletions.
13 changes: 11 additions & 2 deletions ors-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,19 @@
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
</dependency>
<!-- As long as swagger-parser uses snakeyaml <2 it cannot be used. Too much vulnerabilities. -->
<!-- The package is updated quite often. -->
<!-- <dependency>-->
<!-- <groupId>io.swagger.parser.v3</groupId>-->
<!-- <artifactId>swagger-parser</artifactId>-->
<!-- <scope>test</scope>-->
<!-- </dependency>-->

<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
Expand Down
158 changes: 79 additions & 79 deletions ors-api/src/main/java/org/heigit/ors/api/SwaggerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,100 +15,100 @@

package org.heigit.ors.api;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.SpecVersion;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
import org.heigit.ors.config.AppConfig;
import org.springdoc.core.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelKey;
import springfox.documentation.schema.ModelSpecification;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.ModelNamesRegistry;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.ModelNamesRegistryFactoryPlugin;
import springfox.documentation.spi.service.contexts.ModelSpecificationRegistry;
import springfox.documentation.spring.web.paths.DefaultPathProvider;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.scanners.DefaultModelNamesRegistryFactory;
import springfox.documentation.swagger.common.SwaggerPluginSupport;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.time.Duration;
import java.util.Collection;
import java.util.Set;

import javax.servlet.ServletContext;
import java.util.ArrayList;
import java.util.List;

import static org.heigit.ors.api.util.AppInfo.VERSION;


@Configuration
@EnableSwagger2
public class SwaggerConfig {

private final InfoProperties infoProperties;
private static final String SERVICE_NAME = "Openrouteservice";

final
InfoProperties infoProperties;
String swaggerDocumentationAddAutogeneratedUrl = AppConfig.getGlobal().getParameter("info", "swagger_documentation_add_autogenerated_url");

public SwaggerConfig(InfoProperties infoProperties) {
this.infoProperties = infoProperties;
}

ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Openrouteservice")
.description("This is the openrouteservice API documentation")
.license("MIT")
.licenseUrl("https://github.com/swagger-api/swagger-ui/blob/master/LICENSE")
.contact(new Contact("", "", "enquiry@openrouteservice.heigit.org"))
.build();

@Bean
public OpenAPI customOpenAPI(ServletContext servletContext) {
return new OpenAPI(SpecVersion.V31)
.servers(generateServers(servletContext))
.info(apiInfo());
}

/**
* This gives a properly versioned swagger endpoint at api-docs/v2 for the ors v2 version.
* To introduce, e.g., v3 just use this function and replace v2 with v3.
*/
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.host(infoProperties.getSwaggerDocumentationUrl())
.pathProvider(new DefaultPathProvider())
.directModelSubstitute(Duration.class, String.class)
.select()
.apis(RequestHandlerSelectors.basePackage("org.heigit.ors.api"))
.paths(PathSelectors.any())
.build()
.apiInfo(apiInfo());
public GroupedOpenApi orsV2ApiPath() {
String[] paths = {"/v2/**"};
return GroupedOpenApi.builder().group("v2").pathsToMatch(paths)
.build();
}

/**
* This function provides the API v2 at the root api-docs/ path, as this was the old path of service the swagger.
* For proper API versioning see orsV2ApiPath().
*/
@Bean
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER)
public ModelNamesRegistryFactoryPlugin swaggerFixReqResPostfix() {
return new DefaultModelNamesRegistryFactory() {
@Override
public ModelNamesRegistry modelNamesRegistry(ModelSpecificationRegistry registry) {
return super.modelNamesRegistry(hackModelSpecificationRegistry(registry));
}

private ModelSpecificationRegistry hackModelSpecificationRegistry(ModelSpecificationRegistry delegate) {
return new ModelSpecificationRegistry() {
@Override
public ModelSpecification modelSpecificationFor(ModelKey key) {
return delegate.modelSpecificationFor(key);
}

@Override
public boolean hasRequestResponsePairs(ModelKey test) {
return false;
}

@Override
public Collection<ModelKey> modelsDifferingOnlyInValidationGroups(ModelKey test) {
return delegate.modelsDifferingOnlyInValidationGroups(test);
}

@Override
public Collection<ModelKey> modelsWithSameNameAndDifferentNamespace(ModelKey test) {
return delegate.modelsWithSameNameAndDifferentNamespace(test);
}

@Override
public Set<ModelKey> modelKeys() {
return delegate.modelKeys();
}
};
}

};
public GroupedOpenApi oldOrsV2ApiPath() {
String[] paths = {"/v2/**"};
return GroupedOpenApi.builder().group("").pathsToMatch(paths)
.build();
}

private List<Server> generateServers(ServletContext servletContext) {
ArrayList<Server> listOfServers = new ArrayList<>();
if (infoProperties.getSwaggerDocumentationUrl() != null) {
Server customApi = new Server().url(infoProperties.getSwaggerDocumentationUrl()).description("Custom server url");
listOfServers.add(customApi);
}
if (Boolean.parseBoolean(swaggerDocumentationAddAutogeneratedUrl)) {
Server localhostServer = new Server().url(servletContext.getContextPath()).description("Auto generated server url");
listOfServers.add(localhostServer);
}
return listOfServers;
}

private Info apiInfo() {
return new Info()
.title(SERVICE_NAME)
.description("This is the openrouteservice API V2 documentation for ORS Core-Version " + VERSION + ". Documentations for [older Core-Versions](https://github.com/GIScience/openrouteservice-docs/releases) can be rendered with the [Swagger-Editor](https://editor-next.swagger.io/).")
.version("v2")
.contact(apiContact())
.license(apiLicence());
}

private License apiLicence() {
return new License()
.name("MIT Licence")
.url("https://opensource.org/licenses/mit-license.php");
}

private Contact apiContact() {
return new Contact()
.name(SERVICE_NAME)
.email("enquiry@openrouteservice.heigit.org")
.url("https://github.com/GIScience/openrouteservice");
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,21 @@
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
import io.swagger.annotations.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.heigit.ors.api.errors.CommonResponseEntityExceptionHandler;
import org.heigit.ors.api.requests.centrality.CentralityRequest;
import org.heigit.ors.routing.APIEnums;
import org.heigit.ors.api.responses.centrality.json.JsonCentralityResponse;
import org.heigit.ors.centrality.CentralityErrorCodes;
import org.heigit.ors.centrality.CentralityResult;
import org.heigit.ors.exceptions.*;
import org.springdoc.core.annotations.RouterOperation;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConversionException;
Expand All @@ -37,64 +44,71 @@
import javax.servlet.http.HttpServletResponse;

@RestController
@Api(value = "Centrality Service", tags = "Centrality")
@SwaggerDefinition(tags = {
@Tag(name = "Centrality", description = "Get node centrality for different modes of transport")
})
@Tag(name = "Centrality Service", description = "Get node centrality for different modes of transport")
@RequestMapping("/v2/centrality")
@ApiResponses({
@ApiResponse(code = 400, message = "The request is incorrect and therefore can not be processed."),
@ApiResponse(code = 404, message = "An element could not be found. If possible, a more detailed error code is provided."),
@ApiResponse(code = 405, message = "The specified HTTP method is not supported. For more details, refer to the EndPoint documentation."),
@ApiResponse(code = 413, message = "The request is larger than the server is able to process, the data provided in the request exceeds the capacity limit."),
@ApiResponse(code = 500, message = "An unexpected error was encountered and a more detailed error code is provided."),
@ApiResponse(code = 501, message = "Indicates that the server does not support the functionality needed to fulfill the request."),
@ApiResponse(code = 503, message = "The server is currently unavailable due to overload or maintenance.")
})
@ApiResponse(responseCode = "400", description = "The request is incorrect and therefore can not be processed.")
@ApiResponse(responseCode = "404", description = "An element could not be found. If possible, a more detailed error code is provided.")
@ApiResponse(responseCode = "405", description = "The specified HTTP method is not supported. For more details, refer to the EndPoint documentation.")
@ApiResponse(responseCode = "413", description = "The request is larger than the server is able to process, the data provided in the request exceeds the capacity limit.")
@ApiResponse(responseCode = "500", description = "An unexpected error was encountered and a more detailed error code is provided.")
@ApiResponse(responseCode = "501", description = "Indicates that the server does not support the functionality needed to fulfill the request.")
@ApiResponse(responseCode = "503", description = "The server is currently unavailable due to overload or maintenance.")
public class CentralityAPI {
static final CommonResponseEntityExceptionHandler errorHandler = new CommonResponseEntityExceptionHandler(CentralityErrorCodes.BASE);

// generic catch methods - when extra info is provided in the url, the other methods are accessed.
@GetMapping
@ApiOperation(value = "", hidden = true)
@Operation(summary = "", hidden = true)
public void getGetMapping() throws MissingParameterException {
throw new MissingParameterException(CentralityErrorCodes.MISSING_PARAMETER, "profile");
}

@PostMapping
@ApiOperation(value = "", hidden = true)
@Operation(summary = "", hidden = true)
public String getPostMapping(@RequestBody CentralityRequest request) throws MissingParameterException {
throw new MissingParameterException(CentralityErrorCodes.MISSING_PARAMETER, "profile");
}

// Matches any response type that has not been defined
@PostMapping(value = "/{profile}/*")
@ApiOperation(value = "", hidden = true)
@Operation(summary = "", hidden = true)
public void getInvalidResponseType() throws StatusCodeException {
throw new StatusCodeException(HttpServletResponse.SC_NOT_ACCEPTABLE, CentralityErrorCodes.UNSUPPORTED_EXPORT_FORMAT, "This response format is not supported");
}

// Functional request methods
@PostMapping(value = "/{profile}")
@ApiOperation(notes = "Returns an ordered list of points and centrality values within a given bounding box for a selected profile and its settings as JSON", value = "Centrality Service (POST)", httpMethod = "POST", consumes = "application/json", produces = "application/json")
@ApiResponses(
@ApiResponse(code = 200,
message = "Standard response for successfully processed requests. Returns JSON.", //TODO: add docs
response = JsonCentralityResponse.class)
)
public JsonCentralityResponse getDefault(@ApiParam(value = "Specifies the route profile.", required = true, example = "driving-car") @PathVariable APIEnums.Profile profile,
@ApiParam(value = "The request payload", required = true) @RequestBody CentralityRequest request) throws StatusCodeException {
@RouterOperation(operation = @Operation(description = "Returns an ordered list of points and centrality values within a given bounding box for a selected profile and its settings as JSON.", summary = "Centrality Service (POST)"),
method = RequestMethod.POST,
consumes = "application/json",
produces = "application/json")
@ApiResponse(responseCode = "200",
description = "Standard response for successfully processed requests. Returns JSON.",
content = {@Content(
mediaType = "application/geo+json",
array = @ArraySchema(schema = @Schema(implementation = JsonCentralityResponse.class))
)
})
public JsonCentralityResponse getDefault(@Parameter(description = "Specifies the route profile.", required = true, example = "driving-car") @PathVariable APIEnums.Profile profile,
@Parameter(description = "The request payload", required = true) @RequestBody CentralityRequest request) throws StatusCodeException {
return getJsonCentrality(profile, request);
}

@PostMapping(value = "/{profile}/json", produces = {"application/json;charset=UTF-8"})
@ApiOperation(notes = "Returns an ordered list of points and centrality values within a given bounding box for a selected profile and its settings as JSON", value = "Centrality Service JSON (POST)", httpMethod = "POST", consumes = "application/json", produces = "application/json")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "JSON Response", response = JsonCentralityResponse.class)
})
@RouterOperation(operation = @Operation(description = "Returns an ordered list of points and centrality values within a given bounding box for a selected profile and its settings as JSON.", summary = "Centrality Service JSON (POST)"),
method = RequestMethod.POST,
consumes = "application/json",
produces = "application/json")
@ApiResponse(responseCode = "200",
description = "JSON Response.",
content = {@Content(
mediaType = "application/geo+json",
array = @ArraySchema(schema = @Schema(implementation = JsonCentralityResponse.class))
)
})
public JsonCentralityResponse getJsonCentrality(
@ApiParam(value = "Specifies the profile.", required = true, example = "driving-car") @PathVariable APIEnums.Profile profile,
@ApiParam(value = "The request payload", required = true) @RequestBody CentralityRequest request) throws StatusCodeException {
@Parameter(description = "Specifies the profile.", required = true, example = "driving-car") @PathVariable APIEnums.Profile profile,
@Parameter(description = "The request payload", required = true) @RequestBody CentralityRequest request) throws StatusCodeException {
request.setProfile(profile);
request.setResponseType(APIEnums.CentralityResponseType.JSON);

Expand Down
Loading

0 comments on commit b765ce9

Please sign in to comment.