io.swagger.v3.plugin.maven.SwaggerMojo Maven / Gradle / Ivy
package io.swagger.v3.plugin.maven;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import io.swagger.v3.core.filter.OpenAPISpecFilter;
import io.swagger.v3.core.filter.SpecFilter;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.core.util.Yaml;
import io.swagger.v3.jaxrs2.integration.JaxrsOpenApiContextBuilder;
import io.swagger.v3.oas.integration.GenericOpenApiContextBuilder;
import io.swagger.v3.oas.integration.OpenApiConfigurationException;
import io.swagger.v3.oas.integration.SwaggerConfiguration;
import io.swagger.v3.oas.integration.api.OpenApiContext;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.Schema;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.StringUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.BiFunction;
import static java.lang.String.format;
@Mojo(
name = "resolve",
requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
defaultPhase = LifecyclePhase.COMPILE,
threadSafe = true,
configurator = "include-project-dependencies"
)
public class SwaggerMojo extends AbstractMojo {
public enum Format {JSON, YAML, JSONANDYAML}
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (skip) {
getLog().info( "Skipping OpenAPI specification resolution" );
return;
}
getLog().info( "Resolving OpenAPI specification.." );
if (project != null) {
String pEnc = project.getProperties().getProperty("project.build.sourceEncoding");
if (StringUtils.isNotBlank(pEnc)) {
projectEncoding = pEnc;
}
}
if (StringUtils.isBlank(encoding)) {
encoding = projectEncoding;
}
// read swagger configuration if one was provided
Optional swaggerConfiguration =
readStructuredDataFromFile(configurationFilePath, SwaggerConfiguration.class, "configurationFilePath");
// read openApi config, if one was provided
Optional openAPIInput =
readStructuredDataFromFile(openapiFilePath, OpenAPI.class, "openapiFilePath");
config = mergeConfig(openAPIInput.orElse(null), swaggerConfiguration.orElse(new SwaggerConfiguration()));
setDefaultsIfMissing(config);
try {
GenericOpenApiContextBuilder builder = new JaxrsOpenApiContextBuilder()
.openApiConfiguration(config);
if (StringUtils.isNotBlank(contextId)) {
builder.ctxId(contextId);
}
OpenApiContext context = builder.buildContext(true);
OpenAPI openAPI = context.read();
if (StringUtils.isNotBlank(config.getFilterClass())) {
try {
OpenAPISpecFilter filterImpl = (OpenAPISpecFilter) this.getClass().getClassLoader().loadClass(config.getFilterClass()).newInstance();
SpecFilter f = new SpecFilter();
openAPI = f.filter(openAPI, filterImpl, new HashMap<>(), new HashMap<>(),
new HashMap<>());
} catch (Exception e) {
getLog().error("Error applying filter to API specification", e);
throw new MojoExecutionException("Error applying filter to API specification: " + e.getMessage(), e);
}
}
String openapiJson = null;
String openapiYaml = null;
if (Format.JSON.equals(outputFormat) || Format.JSONANDYAML.equals(outputFormat)) {
if (config.isPrettyPrint() != null && config.isPrettyPrint()) {
openapiJson = context.getOutputJsonMapper().writer(new DefaultPrettyPrinter()).writeValueAsString(openAPI);
} else {
openapiJson = context.getOutputJsonMapper().writeValueAsString(openAPI);
}
}
if (Format.YAML.equals(outputFormat) || Format.JSONANDYAML.equals(outputFormat)) {
if (config.isPrettyPrint() != null && config.isPrettyPrint()) {
openapiYaml = context.getOutputYamlMapper().writer(new DefaultPrettyPrinter()).writeValueAsString(openAPI);
} else {
openapiYaml = context.getOutputYamlMapper().writeValueAsString(openAPI);
}
}
Path path = Paths.get(outputPath, "temp");
final File parentFile = path.toFile().getParentFile();
if (parentFile != null) {
parentFile.mkdirs();
}
if (openapiJson != null) {
path = Paths.get(outputPath, outputFileName + ".json");
Files.write(path, openapiJson.getBytes(Charset.forName(encoding)));
getLog().info( "JSON output: " + path.toFile().getCanonicalPath());
}
if (openapiYaml != null) {
path = Paths.get(outputPath, outputFileName + ".yaml");
Files.write(path, openapiYaml.getBytes(Charset.forName(encoding)));
getLog().info( "YAML output: " + path.toFile().getCanonicalPath());
}
} catch (OpenApiConfigurationException e) {
getLog().error( "Error resolving API specification" , e);
throw new MojoFailureException(e.getMessage(), e);
} catch (IOException e) {
getLog().error( "Error writing API specification" , e);
throw new MojoExecutionException("Failed to write API definition", e);
} catch (Exception e) {
getLog().error( "Error resolving API specification" , e);
throw new MojoExecutionException(e.getMessage(), e);
}
}
private void setDefaultsIfMissing(SwaggerConfiguration config) {
if (prettyPrint == null) {
prettyPrint = Boolean.FALSE;
}
if (readAllResources == null) {
readAllResources = Boolean.TRUE;
}
if (sortOutput == null) {
sortOutput = Boolean.FALSE;
}
if (alwaysResolveAppPath == null) {
alwaysResolveAppPath = Boolean.FALSE;
}
if (skipResolveAppPath == null) {
skipResolveAppPath = Boolean.FALSE;
}
if (openapi31 == null) {
openapi31 = Boolean.FALSE;
}
if (convertToOpenAPI31 == null) {
convertToOpenAPI31 = Boolean.FALSE;
}
if (config.isPrettyPrint() == null) {
config.prettyPrint(prettyPrint);
}
if (config.isReadAllResources() == null) {
config.readAllResources(readAllResources);
}
if (config.isSortOutput() == null) {
config.sortOutput(sortOutput);
}
if (config.isAlwaysResolveAppPath() == null) {
config.alwaysResolveAppPath(alwaysResolveAppPath);
}
if (config.isSkipResolveAppPath() == null) {
config.skipResolveAppPath(skipResolveAppPath);
}
if (config.isOpenAPI31() == null) {
config.setOpenAPI31(openapi31);
}
if (config.isConvertToOpenAPI31() == null) {
config.setConvertToOpenAPI31(convertToOpenAPI31);
}
}
/**
* Read the content of given file as either json or yaml and maps it to given class
*
* @param filePath to read content from
* @param outputClass to map to
* @param configName for logging, what user config will be read
* @param mapped type
* @return empty optional if not path was given or the file was empty, read instance otherwis
* @throws MojoFailureException if given path is not file, could not be read or is not proper json or yaml
*/
private Optional readStructuredDataFromFile(String filePath, Class outputClass, String configName)
throws MojoFailureException {
try {
// ignore if config is not provided
if (StringUtils.isBlank(filePath)) {
return Optional.empty();
}
Path pathObj = Paths.get(filePath);
// if file does not exist or is not an actual file, finish with error
if (!pathObj.toFile().exists() || !pathObj.toFile().isFile()) {
throw new IllegalArgumentException(
format("passed path does not exist or is not a file: '%s'", filePath));
}
String fileContent = new String(Files.readAllBytes(pathObj), encoding);
// if provided file is empty, log warning and finish
if (StringUtils.isBlank(fileContent)) {
getLog().warn(format("It seems that file '%s' defined in config %s is empty",
pathObj.toString(), configName));
return Optional.empty();
}
// get mappers in the order based on file extension
List, T>> mappers = getSortedMappers(pathObj);
T instance = null;
List caughtExs = new ArrayList<>();
// iterate through mappers and see if one is able to parse
for (BiFunction, T> mapper : mappers) {
try {
instance = mapper.apply(fileContent, outputClass);
break;
} catch (Exception e) {
caughtExs.add(e);
}
}
// if no mapper could read the content correctly, finish with error
if (instance == null) {
if (caughtExs.isEmpty()) {
caughtExs.add(new IllegalStateException("undefined state"));
}
// we give more importance to the first exception, it was produced by the preferred mapper
Throwable caughtEx = caughtExs.get(0);
getLog().error(format("Could not read file '%s' for config %s", pathObj, configName), caughtEx);
if(caughtExs.size() > 1){
for (Throwable ex : caughtExs.subList(1, caughtExs.size())) {
getLog().warn(format("Also could not read file '%s' for config %s with alternate mapper", pathObj, configName), ex);
}
}
throw new IllegalStateException(caughtEx.getMessage(), caughtEx);
}
return Optional.of(instance);
} catch (Exception e) {
getLog().error(format("Error reading/deserializing config %s file", configName), e);
throw new MojoFailureException(e.getMessage(), e);
}
}
/**
* Get sorted list of mappers based on given filename.
*
* Will sort the 2 supported mappers: json and yaml based on what file extension is used.
*
* @param pathObj to get extension from.
* @param mapped type
* @return list of mappers
*/
private List, T>> getSortedMappers(Path pathObj) {
String ext = FileUtils.extension(pathObj.toString());
boolean yamlPreferred = false;
if (ext.equalsIgnoreCase("yaml") || ext.equalsIgnoreCase("yml")) {
yamlPreferred = true;
}
List, T>> list = new ArrayList<>(2);
list.add((content, typeClass) -> {
try {
return Json.mapper().readValue(content, typeClass);
} catch (IOException e) {
throw new IllegalStateException(e);
}
});
list.add((content, typeClass) -> {
try {
return Yaml.mapper().readValue(content, typeClass);
} catch (IOException e) {
throw new IllegalStateException(e);
}
});
if (yamlPreferred) {
Collections.reverse(list);
}
return Collections.unmodifiableList(list);
}
private SwaggerConfiguration mergeConfig(OpenAPI openAPIInput, SwaggerConfiguration config) {
// overwrite all settings provided by other maven config
if (StringUtils.isNotBlank(filterClass)) {
config.filterClass(filterClass);
}
if (isCollectionNotBlank(ignoredRoutes)) {
config.ignoredRoutes(ignoredRoutes);
}
if (prettyPrint != null) {
config.prettyPrint(prettyPrint);
}
if (sortOutput != null) {
config.sortOutput(sortOutput);
}
if (alwaysResolveAppPath != null) {
config.alwaysResolveAppPath(alwaysResolveAppPath);
}
if (skipResolveAppPath != null) {
config.skipResolveAppPath(skipResolveAppPath);
}
if (readAllResources != null) {
config.readAllResources(readAllResources);
}
if (StringUtils.isNotBlank(readerClass)) {
config.readerClass(readerClass);
}
if (StringUtils.isNotBlank(scannerClass)) {
config.scannerClass(scannerClass);
}
if (isCollectionNotBlank(resourceClasses)) {
config.resourceClasses(resourceClasses);
}
if (openAPIInput != null) {
config.openAPI(openAPIInput);
}
if (isCollectionNotBlank(resourcePackages)) {
config.resourcePackages(resourcePackages);
}
if (StringUtils.isNotBlank(objectMapperProcessorClass)) {
config.objectMapperProcessorClass(objectMapperProcessorClass);
}
if (StringUtils.isNotBlank(defaultResponseCode)) {
config.defaultResponseCode(defaultResponseCode);
}
if (isCollectionNotBlank(modelConverterClasses)) {
config.modelConverterClasses(modelConverterClasses);
}
if (openapi31 != null) {
config.openAPI31(openapi31);
}
if (StringUtils.isNotBlank(schemaResolution)) {
config.schemaResolution(Schema.SchemaResolution.valueOf(schemaResolution));
}
return config;
}
private boolean isCollectionNotBlank(Collection> collection) {
return collection != null && !collection.isEmpty();
}
@Parameter( property = "resolve.outputFileName", defaultValue = "openapi")
private String outputFileName = "openapi";
@Parameter( property = "resolve.outputPath" )
private String outputPath;
@Parameter( property = "resolve.outputFormat", defaultValue = "JSON")
private Format outputFormat = Format.JSON;
@Parameter( property = "resolve.resourcePackages" )
private Set resourcePackages;
@Parameter( property = "resolve.resourceClasses" )
private Set resourceClasses;
@Parameter( property = "resolve.modelConverterClasses" )
private LinkedHashSet modelConverterClasses;
@Parameter( property = "resolve.filterClass" )
private String filterClass;
@Parameter( property = "resolve.readerClass" )
private String readerClass;
@Parameter( property = "resolve.scannerClass" )
private String scannerClass;
/**
* @since 2.0.6
*/
@Parameter( property = "resolve.objectMapperProcessorClass" )
private String objectMapperProcessorClass;
/**
* @since 2.2.17
*/
@Parameter( property = "resolve.defaultResponseCode" )
private String defaultResponseCode;
@Parameter(property = "resolve.prettyPrint")
private Boolean prettyPrint;
@Parameter(property = "resolve.readAllResources")
private Boolean readAllResources;
@Parameter( property = "resolve.ignoredRoutes" )
private Collection ignoredRoutes;
/**
* @since 2.0.6
*/
@Parameter(property = "resolve.contextId", defaultValue = "${project.artifactId}")
private String contextId;
@Parameter( property = "resolve.skip" )
private Boolean skip = Boolean.FALSE;
@Parameter( property = "resolve.openapiFilePath")
private String openapiFilePath;
/**
* @since 2.0.8
*/
@Parameter(property = "resolve.configurationFilePath")
private String configurationFilePath;
@Parameter(defaultValue = "${project}", readonly = true)
private MavenProject project;
@Parameter( property = "resolve.encoding" )
private String encoding;
/**
* @since 2.1.6
*/
@Parameter(property = "resolve.sortOutput")
private Boolean sortOutput;
/**
* @since 2.1.9
*/
@Parameter(property = "resolve.alwaysResolveAppPath")
private Boolean alwaysResolveAppPath;
/**
* @since 2.1.15
*/
@Parameter(property = "resolve.skipResolveAppPath")
private Boolean skipResolveAppPath;
/**
* @since 2.2.0
*/
@Parameter(property = "resolve.openapi31")
private Boolean openapi31;
/**
* @since 2.2.12
*/
@Parameter(property = "resolve.convertToOpenAPI31")
private Boolean convertToOpenAPI31;
/**
* @since 2.2.24
*/
@Parameter(property = "resolve.schemaResolution")
private String schemaResolution;
private String projectEncoding = "UTF-8";
private SwaggerConfiguration config;
public String getOutputPath() {
return outputPath;
}
public String getOpenapiFilePath() {
return openapiFilePath;
}
String getConfigurationFilePath() {
return configurationFilePath;
}
void setContextId(String contextId) {
this.contextId = contextId;
}
SwaggerConfiguration getInternalConfiguration() {
return config;
}
}