org.wildfly.swarm.microprofile.openapi.runtime.OpenApiAnnotationScanner Maven / Gradle / Ivy
/**
* Copyright 2018 Red Hat, Inc, and individual contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.wildfly.swarm.microprofile.openapi.runtime;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.ws.rs.core.Application;
import org.apache.commons.beanutils.PropertyUtils;
import org.eclipse.microprofile.openapi.annotations.enums.Explode;
import org.eclipse.microprofile.openapi.models.Components;
import org.eclipse.microprofile.openapi.models.ExternalDocumentation;
import org.eclipse.microprofile.openapi.models.OpenAPI;
import org.eclipse.microprofile.openapi.models.Operation;
import org.eclipse.microprofile.openapi.models.PathItem;
import org.eclipse.microprofile.openapi.models.PathItem.HttpMethod;
import org.eclipse.microprofile.openapi.models.Paths;
import org.eclipse.microprofile.openapi.models.callbacks.Callback;
import org.eclipse.microprofile.openapi.models.examples.Example;
import org.eclipse.microprofile.openapi.models.headers.Header;
import org.eclipse.microprofile.openapi.models.info.Contact;
import org.eclipse.microprofile.openapi.models.info.Info;
import org.eclipse.microprofile.openapi.models.info.License;
import org.eclipse.microprofile.openapi.models.links.Link;
import org.eclipse.microprofile.openapi.models.media.Content;
import org.eclipse.microprofile.openapi.models.media.Discriminator;
import org.eclipse.microprofile.openapi.models.media.Encoding;
import org.eclipse.microprofile.openapi.models.media.MediaType;
import org.eclipse.microprofile.openapi.models.media.Schema;
import org.eclipse.microprofile.openapi.models.parameters.Parameter;
import org.eclipse.microprofile.openapi.models.parameters.Parameter.In;
import org.eclipse.microprofile.openapi.models.parameters.RequestBody;
import org.eclipse.microprofile.openapi.models.responses.APIResponse;
import org.eclipse.microprofile.openapi.models.responses.APIResponses;
import org.eclipse.microprofile.openapi.models.security.OAuthFlow;
import org.eclipse.microprofile.openapi.models.security.OAuthFlows;
import org.eclipse.microprofile.openapi.models.security.Scopes;
import org.eclipse.microprofile.openapi.models.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.models.security.SecurityScheme;
import org.eclipse.microprofile.openapi.models.servers.Server;
import org.eclipse.microprofile.openapi.models.servers.ServerVariable;
import org.eclipse.microprofile.openapi.models.servers.ServerVariables;
import org.eclipse.microprofile.openapi.models.tags.Tag;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.ClassType;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.Indexer;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.MethodParameterInfo;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ArchivePath;
import org.jboss.shrinkwrap.api.Node;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.importer.ZipImporter;
import org.wildfly.swarm.microprofile.openapi.api.OpenApiConfig;
import org.wildfly.swarm.microprofile.openapi.api.models.ComponentsImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.ExternalDocumentationImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.OpenAPIImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.OperationImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.PathItemImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.PathsImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.callbacks.CallbackImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.examples.ExampleImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.headers.HeaderImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.info.ContactImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.info.InfoImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.info.LicenseImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.links.LinkImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.media.ContentImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.media.DiscriminatorImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.media.EncodingImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.media.MediaTypeImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.media.SchemaImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.parameters.ParameterImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.parameters.RequestBodyImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.responses.APIResponseImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.responses.APIResponsesImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.security.OAuthFlowImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.security.OAuthFlowsImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.security.ScopesImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.security.SecurityRequirementImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.security.SecuritySchemeImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.servers.ServerImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.servers.ServerVariableImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.servers.ServerVariablesImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.tags.TagImpl;
import org.wildfly.swarm.microprofile.openapi.api.util.MergeUtil;
import org.wildfly.swarm.microprofile.openapi.runtime.scanner.OpenApiDataObjectScanner;
import org.wildfly.swarm.microprofile.openapi.runtime.util.JandexUtil;
import org.wildfly.swarm.microprofile.openapi.runtime.util.JandexUtil.JaxRsParameterInfo;
import org.wildfly.swarm.microprofile.openapi.runtime.util.JandexUtil.RefType;
import org.wildfly.swarm.microprofile.openapi.runtime.util.ModelUtil;
import org.wildfly.swarm.spi.api.JARArchive;
/**
* Scans a deployment (using the archive and jandex annotation index) for JAX-RS and
* OpenAPI annotations. These annotations, if found, are used to generate a valid
* OpenAPI model. For reference, see:
*
* https://github.com/eclipse/microprofile-open-api/blob/master/spec/src/main/asciidoc/microprofile-openapi-spec.adoc#annotations
*
* @author [email protected]
*/
@SuppressWarnings("rawtypes")
public class OpenApiAnnotationScanner {
private static Logger LOG = Logger.getLogger("org.wildfly.swarm.microprofile.openapi");
private final IndexView index;
private OpenAPIImpl oai;
private String currentAppPath = "";
private String currentResourcePath = "";
private String[] currentConsumes;
private String[] currentProduces;
private SchemaRegistry schemaRegistry = new SchemaRegistry();
/**
* Constructor.
* @param config
* @param archive
*/
public OpenApiAnnotationScanner(OpenApiConfig config, Archive archive) {
this.index = archiveToIndex(config, archive);
}
/**
* Index the archive to produce a jandex index.
* @param config
* @param archive
*/
protected static IndexView archiveToIndex(OpenApiConfig config, Archive archive) {
if (archive == null) {
throw new RuntimeException("Archive was null!");
}
Indexer indexer = new Indexer();
index(indexer, "org/wildfly/swarm/microprofile/openapi/runtime/scanner/CollectionStandin.class");
index(indexer, "org/wildfly/swarm/microprofile/openapi/runtime/scanner/MapStandin.class");
indexArchive(config, indexer, archive);
return indexer.complete();
}
private static void index(Indexer indexer, String resName) {
ClassLoader cl = OpenApiAnnotationScanner.class.getClassLoader();
try (InputStream klazzStream = cl.getResourceAsStream(resName)) {
indexer.index(klazzStream);
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
}
/**
* Indexes the given archive.
* @param config
* @param indexer
* @param archive
*/
@SuppressWarnings("unchecked")
private static void indexArchive(OpenApiConfig config, Indexer indexer, Archive archive) {
Map c = archive.getContent();
try {
for (Map.Entry each : c.entrySet()) {
ArchivePath archivePath = each.getKey();
if (archivePath.get().endsWith(OpenApiConstants.CLASS_SUFFIX) && acceptClassForScanning(config, archivePath.get())) {
try (InputStream contentStream = each.getValue().getAsset().openStream()) {
LOG.debugv("Indexing asset: {0} from archive: {1}", archivePath.get(), archive.getName());
indexer.index(contentStream);
}
continue;
}
if (archivePath.get().endsWith(OpenApiConstants.JAR_SUFFIX) && acceptJarForScanning(config, archivePath.get())) {
try (InputStream contentStream = each.getValue().getAsset().openStream()) {
JARArchive jarArchive = ShrinkWrap.create(JARArchive.class, archivePath.get())
.as(ZipImporter.class).importFrom(contentStream).as(JARArchive.class);
indexArchive(config, indexer, jarArchive);
}
continue;
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Returns true if the given JAR archive (dependency) should be cracked open and indexed
* along with the rest of the deployment's classes.
* @param config
* @param jarName
*/
private static boolean acceptJarForScanning(OpenApiConfig config, String jarName) {
if (config.scanDependenciesDisable()) {
return false;
}
Set scanDependenciesJars = config.scanDependenciesJars();
String nameOnly = new File(jarName).getName();
if (scanDependenciesJars.isEmpty() || scanDependenciesJars.contains(nameOnly)) {
return true;
}
return false;
}
/**
* Returns true if the class represented by the given archive path should be included in
* the annotation index.
* @param config
* @param archivePath
*/
private static boolean acceptClassForScanning(OpenApiConfig config, String archivePath) {
if (archivePath == null) {
return false;
}
Set scanClasses = config.scanClasses();
Set scanPackages = config.scanPackages();
Set scanExcludeClasses = config.scanExcludeClasses();
Set scanExcludePackages = config.scanExcludePackages();
if (scanClasses.isEmpty() && scanPackages.isEmpty() && scanExcludeClasses.isEmpty() && scanExcludePackages.isEmpty()) {
return true;
}
if (archivePath.startsWith(OpenApiConstants.WEB_ARCHIVE_CLASS_PREFIX)) {
archivePath = archivePath.substring(OpenApiConstants.WEB_ARCHIVE_CLASS_PREFIX.length());
}
String fqcn = archivePath.replaceAll("/", ".").substring(0, archivePath.lastIndexOf(OpenApiConstants.CLASS_SUFFIX));
String packageName = "";
if (fqcn.contains(".")) {
int idx = fqcn.lastIndexOf(".");
packageName = fqcn.substring(0, idx);
}
boolean accept;
// Includes
if (scanClasses.isEmpty() && scanPackages.isEmpty()) {
accept = true;
} else if (!scanClasses.isEmpty() && scanPackages.isEmpty()) {
accept = scanClasses.contains(fqcn);
} else if (scanClasses.isEmpty() && !scanPackages.isEmpty()) {
accept = scanPackages.contains(packageName);
} else {
accept = scanClasses.contains(fqcn) || scanPackages.contains(packageName);
}
// Excludes override includes
if (!scanExcludeClasses.isEmpty() && scanExcludeClasses.contains(fqcn)) {
accept = false;
}
if (!scanExcludePackages.isEmpty() && scanExcludePackages.contains(packageName)) {
accept = false;
}
return accept;
}
/**
* Scan the deployment for relevant annotations. Returns an OpenAPI data model that was
* built from those found annotations.
*/
public OpenAPIImpl scan() {
LOG.debug("Scanning deployment for OpenAPI and JAX-RS Annotations.");
// Initialize a new OAI document. Even if nothing is found, this will be returned.
oai = new OpenAPIImpl();
oai.setOpenapi(OpenApiConstants.OPEN_API_VERSION);
// Get all jax-rs applications and convert them to OAI models (and merge them into a single one)
Collection applications = this.index.getAllKnownSubclasses(DotName.createSimple(Application.class.getName()));
for (ClassInfo classInfo : applications) {
oai = MergeUtil.merge(oai, jaxRsApplicationToOpenApi(classInfo));
}
// TODO find all OpenAPIDefinition annotations at the package level
// Now find all jax-rs endpoints
Collection resourceClasses = JandexUtil.getJaxRsResourceClasses(this.index);
for (ClassInfo resourceClass : resourceClasses) {
processJaxRsResourceClass(oai, resourceClass);
}
// Now that all paths have been created, sort them (we don't have a better way to organize them).
if (oai != null) {
Paths paths = oai.getPaths();
if (paths != null) {
Paths sortedPaths = new PathsImpl();
TreeSet sortedKeys = new TreeSet<>(paths.keySet());
for (String pathKey : sortedKeys) {
PathItem pathItem = paths.get(pathKey);
sortedPaths.addPathItem(pathKey, pathItem);
}
sortedPaths.setExtensions(paths.getExtensions());
oai.setPaths(sortedPaths);
}
}
return oai;
}
/**
* Processes a JAX-RS {@link Application} and creates an {@link OpenAPI} model. Performs
* annotation scanning and other processing. Returns a model unique to that single JAX-RS
* app.
* @param applicationClass
*/
private OpenAPIImpl jaxRsApplicationToOpenApi(ClassInfo applicationClass) {
OpenAPIImpl oai = new OpenAPIImpl();
oai.setOpenapi(OpenApiConstants.OPEN_API_VERSION);
// Get the @ApplicationPath info and save it for later (also support @Path which seems nonstandard but common).
////////////////////////////////////////
AnnotationInstance appPathAnno = JandexUtil.getClassAnnotation(applicationClass, OpenApiConstants.DOTNAME_APPLICATION_PATH);
if (appPathAnno == null) {
appPathAnno = JandexUtil.getClassAnnotation(applicationClass, OpenApiConstants.DOTNAME_PATH);
}
if (appPathAnno != null) {
this.currentAppPath = appPathAnno.value().asString();
} else {
this.currentAppPath = "/";
}
// Get the @OpenAPIDefinition annotation and process it.
////////////////////////////////////////
AnnotationInstance openApiDefAnno = JandexUtil.getClassAnnotation(applicationClass, OpenApiConstants.DOTNAME_OPEN_API_DEFINITION);
if (openApiDefAnno != null) {
processDefinition(oai, openApiDefAnno);
}
// Process @SecurityScheme annotations
////////////////////////////////////////
List securitySchemeAnnotations = JandexUtil.getRepeatableAnnotation(applicationClass,
OpenApiConstants.DOTNAME_SECURITY_SCHEME, OpenApiConstants.DOTNAME_SECURITY_SCHEMES);
for (AnnotationInstance annotation : securitySchemeAnnotations) {
String name = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_SECURITY_SCHEME_NAME);
if (name == null && JandexUtil.isRef(annotation)) {
name = JandexUtil.nameFromRef(annotation);
}
if (name != null) {
SecurityScheme securityScheme = readSecurityScheme(annotation);
Components components = ModelUtil.components(oai);
components.addSecurityScheme(name, securityScheme);
}
}
// Process @Server annotations
///////////////////////////////////
List serverAnnotations = JandexUtil.getRepeatableAnnotation(applicationClass,
OpenApiConstants.DOTNAME_SERVER, OpenApiConstants.DOTNAME_SERVERS);
for (AnnotationInstance annotation : serverAnnotations) {
Server server = readServer(annotation);
oai.addServer(server);
}
return oai;
}
/**
* Processing a single JAX-RS resource class (annotated with @Path).
* @param openApi
* @param resourceClass
*/
private void processJaxRsResourceClass(OpenAPIImpl openApi, ClassInfo resourceClass) {
LOG.debug("Processing a JAX-RS resource class: " + resourceClass.simpleName());
// Set the current resource path.
AnnotationInstance pathAnno = JandexUtil.getClassAnnotation(resourceClass, OpenApiConstants.DOTNAME_PATH);
this.currentResourcePath = pathAnno.value().asString();
// TODO handle the use-case where the resource class extends a base class, and the base class has jax-rs relevant methods and annotations
// Process @SecurityScheme annotations
////////////////////////////////////////
List securitySchemeAnnotations = JandexUtil.getRepeatableAnnotation(resourceClass,
OpenApiConstants.DOTNAME_SECURITY_SCHEME, OpenApiConstants.DOTNAME_SECURITY_SCHEMES);
for (AnnotationInstance annotation : securitySchemeAnnotations) {
String name = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_SECURITY_SCHEME_NAME);
if (name == null && JandexUtil.isRef(annotation)) {
name = JandexUtil.nameFromRef(annotation);
}
if (name != null) {
SecurityScheme securityScheme = readSecurityScheme(annotation);
Components components = ModelUtil.components(openApi);
components.addSecurityScheme(name, securityScheme);
}
}
// Process tags (both declarations and references)
////////////////////////////////////////
Set tagRefs = new HashSet<>();
AnnotationInstance tagAnno = JandexUtil.getClassAnnotation(resourceClass, OpenApiConstants.DOTNAME_TAG);
if (tagAnno != null) {
if (JandexUtil.isRef(tagAnno)) {
String tagRef = JandexUtil.stringValue(tagAnno, OpenApiConstants.PROP_REF);
tagRefs.add(tagRef);
} else {
Tag tag = readTag(tagAnno);
if (tag.getName() != null) {
openApi.addTag(tag);
tagRefs.add(tag.getName());
}
}
}
AnnotationInstance tagsAnno = JandexUtil.getClassAnnotation(resourceClass, OpenApiConstants.DOTNAME_TAGS);
if (tagsAnno != null) {
AnnotationValue tagsArrayVal = tagsAnno.value();
if (tagsArrayVal != null) {
AnnotationInstance[] tagsArray = tagsArrayVal.asNestedArray();
for (AnnotationInstance ta : tagsArray) {
if (JandexUtil.isRef(ta)) {
String tagRef = JandexUtil.stringValue(ta, OpenApiConstants.PROP_REF);
tagRefs.add(tagRef);
} else {
Tag tag = readTag(ta);
if (tag.getName() != null) {
openApi.addTag(tag);
tagRefs.add(tag.getName());
}
}
}
}
List listValue = JandexUtil.stringListValue(tagsAnno, OpenApiConstants.PROP_REFS);
if (listValue != null) {
tagRefs.addAll(listValue);
}
}
// Now find and process the operation methods
////////////////////////////////////////
for (MethodInfo methodInfo : resourceClass.methods()) {
AnnotationInstance get = methodInfo.annotation(OpenApiConstants.DOTNAME_GET);
if (get != null) {
processJaxRsMethod(openApi, resourceClass, methodInfo, get, HttpMethod.GET, tagRefs);
}
AnnotationInstance put = methodInfo.annotation(OpenApiConstants.DOTNAME_PUT);
if (put != null) {
processJaxRsMethod(openApi, resourceClass, methodInfo, put, HttpMethod.PUT, tagRefs);
}
AnnotationInstance post = methodInfo.annotation(OpenApiConstants.DOTNAME_POST);
if (post != null) {
processJaxRsMethod(openApi, resourceClass, methodInfo, post, HttpMethod.POST, tagRefs);
}
AnnotationInstance delete = methodInfo.annotation(OpenApiConstants.DOTNAME_DELETE);
if (delete != null) {
processJaxRsMethod(openApi, resourceClass, methodInfo, delete, HttpMethod.DELETE, tagRefs);
}
AnnotationInstance head = methodInfo.annotation(OpenApiConstants.DOTNAME_HEAD);
if (head != null) {
processJaxRsMethod(openApi, resourceClass, methodInfo, head, HttpMethod.HEAD, tagRefs);
}
AnnotationInstance options = methodInfo.annotation(OpenApiConstants.DOTNAME_OPTIONS);
if (options != null) {
processJaxRsMethod(openApi, resourceClass, methodInfo, options, HttpMethod.OPTIONS, tagRefs);
}
}
}
/**
* Process a single JAX-RS method to produce an OpenAPI Operation.
* @param openApi
* @param resource
* @param method
* @param methodAnno
* @param methodType
* @param resourceTags
*/
private void processJaxRsMethod(OpenAPIImpl openApi, ClassInfo resource, MethodInfo method,
AnnotationInstance methodAnno, HttpMethod methodType, Set resourceTags) {
LOG.debugf("Processing jax-rs method: {0}", method.toString());
// Figure out the path for the operation. This is a combination of the App, Resource, and Method @Path annotations
String path;
if (method.hasAnnotation(OpenApiConstants.DOTNAME_PATH)) {
AnnotationInstance pathAnno = method.annotation(OpenApiConstants.DOTNAME_PATH);
String methodPath = pathAnno.value().asString();
path = makePath(this.currentAppPath, this.currentResourcePath, methodPath);
} else {
path = makePath(this.currentAppPath, this.currentResourcePath);
}
// Get or create a PathItem to hold the operation
PathItem pathItem = ModelUtil.paths(openApi).get(path);
if (pathItem == null) {
pathItem = new PathItemImpl();
ModelUtil.paths(openApi).addPathItem(path, pathItem);
}
// Figure out the current @Produces and @Consumes (if any)
currentConsumes = null;
currentProduces = null;
AnnotationInstance consumesAnno = method.annotation(OpenApiConstants.DOTNAME_CONSUMES);
if (consumesAnno == null) {
consumesAnno = JandexUtil.getClassAnnotation(method.declaringClass(), OpenApiConstants.DOTNAME_CONSUMES);
}
AnnotationInstance producesAnno = method.annotation(OpenApiConstants.DOTNAME_PRODUCES);
if (producesAnno == null) {
producesAnno = JandexUtil.getClassAnnotation(method.declaringClass(), OpenApiConstants.DOTNAME_PRODUCES);
}
if (consumesAnno != null) {
AnnotationValue annotationValue = consumesAnno.value();
if (annotationValue != null) {
currentConsumes = annotationValue.asStringArray();
} else {
currentConsumes = OpenApiConstants.DEFAULT_CONSUMES;
}
}
if (producesAnno != null) {
AnnotationValue annotationValue = producesAnno.value();
if (annotationValue != null) {
currentProduces = annotationValue.asStringArray();
} else {
currentProduces = OpenApiConstants.DEFAULT_PRODUCES;
}
}
Operation operation = new OperationImpl();
// Process any @Operation annotation
/////////////////////////////////////////
if (method.hasAnnotation(OpenApiConstants.DOTNAME_OPERATION)) {
AnnotationInstance operationAnno = method.annotation(OpenApiConstants.DOTNAME_OPERATION);
// If the operation is marked as hidden, just bail here because we don't want it as part of the model.
if (operationAnno.value(OpenApiConstants.PROP_HIDDEN) != null && operationAnno.value(OpenApiConstants.PROP_HIDDEN).asBoolean()) {
return;
}
// Otherwise, set various bits of meta-data from the values in the @Operation annotation
operation.setSummary(JandexUtil.stringValue(operationAnno, OpenApiConstants.PROP_SUMMARY));
operation.setDescription(JandexUtil.stringValue(operationAnno, OpenApiConstants.PROP_DESCRIPTION));
operation.setOperationId(JandexUtil.stringValue(operationAnno, OpenApiConstants.PROP_OPERATION_ID));
operation.setDeprecated(JandexUtil.booleanValue(operationAnno, OpenApiConstants.PROP_DEPRECATED));
}
// Process tags - @Tag and @Tags annotations combines with the resource tags we've already found (passed in)
/////////////////////////////////////////
boolean hasOpTags = false;
Set tags = new HashSet<>();
if (method.hasAnnotation(OpenApiConstants.DOTNAME_TAG)) {
hasOpTags = true;
AnnotationInstance tagAnno = method.annotation(OpenApiConstants.DOTNAME_TAG);
if (JandexUtil.isRef(tagAnno)) {
String tagRef = JandexUtil.stringValue(tagAnno, OpenApiConstants.PROP_REF);
tags.add(tagRef);
} else if (JandexUtil.isEmpty(tagAnno)) {
// Nothing to do here. The @Tag() was empty.
} else {
Tag tag = readTag(tagAnno);
if (tag.getName() != null) {
openApi.addTag(tag);
tags.add(tag.getName());
}
}
}
if (method.hasAnnotation(OpenApiConstants.DOTNAME_TAGS)) {
hasOpTags = true;
AnnotationInstance tagsAnno = method.annotation(OpenApiConstants.DOTNAME_TAGS);
AnnotationValue tagsArrayVal = tagsAnno.value();
if (tagsArrayVal != null) {
AnnotationInstance[] tagsArray = tagsArrayVal.asNestedArray();
for (AnnotationInstance tagAnno : tagsArray) {
if (JandexUtil.isRef(tagAnno)) {
String tagRef = JandexUtil.stringValue(tagAnno, OpenApiConstants.PROP_REF);
tags.add(tagRef);
} else {
Tag tag = readTag(tagAnno);
if (tag.getName() != null) {
openApi.addTag(tag);
tags.add(tag.getName());
}
}
}
}
List listValue = JandexUtil.stringListValue(tagsAnno, OpenApiConstants.PROP_REFS);
if (listValue != null) {
tags.addAll(listValue);
}
}
if (!hasOpTags) {
tags.addAll(resourceTags);
}
if (!tags.isEmpty()) {
operation.setTags(new ArrayList<>(tags));
}
// Process @Parameter annotations
/////////////////////////////////////////
List parameterAnnotations = JandexUtil.getRepeatableAnnotation(method,
OpenApiConstants.DOTNAME_PARAMETER, OpenApiConstants.DOTNAME_PARAMETERS);
for (AnnotationInstance annotation : parameterAnnotations) {
Parameter parameter = readParameter(annotation);
if (parameter == null) {
// Param was hidden
continue;
}
AnnotationTarget target = annotation.target();
// If target is null, then the @Parameter was found wrapped in a @Parameters
// If the target is METHOD, then the @Parameter is on the method itself
// If the target is METHOD_PARAMETER, then the @Parameter is on one of the method's arguments (THIS ONE WE CARE ABOUT)
if (target != null && target.kind() == Kind.METHOD_PARAMETER) {
In in = parameterIn(target.asMethodParameter());
parameter.setIn(in);
// if the Parameter model we read does *NOT* have a Schema at this point, then create one from the method argument's type
if (!ModelUtil.parameterHasSchema(parameter)) {
Type paramType = JandexUtil.getMethodParameterType(method, target.asMethodParameter().position());
Schema schema = typeToSchema(paramType);
ModelUtil.setParameterSchema(parameter, schema);
}
} else {
// TODO if the @Parameter is on the method, we could perhaps find the one it refers to by name
// and use its type to create a Schema (if missing)
}
operation.addParameter(parameter);
}
// Now process any jax-rs parameters that were NOT annotated with @Parameter (do not yet exist in the model)
List parameters = method.parameters();
for (int idx = 0; idx < parameters.size(); idx++) {
JaxRsParameterInfo paramInfo = JandexUtil.getMethodParameterJaxRsInfo(method, idx);
if (paramInfo != null && !ModelUtil.operationHasParameter(operation, paramInfo.name)) {
Type paramType = parameters.get(idx);
Parameter parameter = new ParameterImpl();
parameter.setName(paramInfo.name);
parameter.setIn(paramInfo.in);
parameter.setRequired(true);
Schema schema = typeToSchema(paramType);
parameter.setSchema(schema);
operation.addParameter(parameter);
}
}
// TODO @Parameter can be located on a field - what does that mean?
// TODO need to handle the case where we have @BeanParam annotations
// Process any @RequestBody annotation
/////////////////////////////////////////
// note: the @RequestBody annotation can be found on a method argument *or* on the method
List requestBodyAnnotations = JandexUtil.getRepeatableAnnotation(method, OpenApiConstants.DOTNAME_REQUEST_BODY, null);
for (AnnotationInstance annotation : requestBodyAnnotations) {
RequestBody requestBody = readRequestBody(annotation);
// TODO if the method argument type is Request, don't generate a Schema!
if (!ModelUtil.requestBodyHasSchema(requestBody)) {
Type requestBodyType = null;
if (annotation.target().kind() == AnnotationTarget.Kind.METHOD_PARAMETER) {
requestBodyType = JandexUtil.getMethodParameterType(method, annotation.target().asMethodParameter().position());
} else if (annotation.target().kind() == AnnotationTarget.Kind.METHOD) {
requestBodyType = JandexUtil.getRequestBodyParameterClassType(method);
}
if (requestBodyType != null) {
Schema schema = typeToSchema(requestBodyType);
ModelUtil.setRequestBodySchema(requestBody, schema, currentConsumes);
}
}
operation.setRequestBody(requestBody);
}
// If the request body is null, figure it out from the parameters. Only if the
// method declares that it @Consumes data
if (operation.getRequestBody() == null && currentConsumes != null) {
Type requestBodyType = JandexUtil.getRequestBodyParameterClassType(method);
if (requestBodyType != null) {
Schema schema = typeToSchema(requestBodyType);
if (schema != null) {
RequestBody requestBody = new RequestBodyImpl();
ModelUtil.setRequestBodySchema(requestBody, schema, currentConsumes);
operation.setRequestBody(requestBody);
}
}
}
// Process @APIResponse annotations
/////////////////////////////////////////
List apiResponseAnnotations = JandexUtil.getRepeatableAnnotation(method,
OpenApiConstants.DOTNAME_API_RESPONSE, OpenApiConstants.DOTNAME_API_RESPONSES);
for (AnnotationInstance annotation : apiResponseAnnotations) {
String responseCode = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_RESPONSE_CODE);
if (responseCode == null) {
responseCode = APIResponses.DEFAULT;
}
APIResponse response = readResponse(annotation);
APIResponses responses = ModelUtil.responses(operation);
responses.addApiResponse(responseCode, response);
}
// If there are no responses from annotations, try to create a response from the method return value.
if (operation.getResponses() == null || operation.getResponses().isEmpty()) {
createResponseFromJaxRsMethod(method, operation);
}
// Process @SecurityRequirement annotations
///////////////////////////////////////////
List securityRequirementAnnotations = JandexUtil.getRepeatableAnnotation(method,
OpenApiConstants.DOTNAME_SECURITY_REQUIREMENT, OpenApiConstants.DOTNAME_SECURITY_REQUIREMENTS);
securityRequirementAnnotations.addAll(
JandexUtil.getRepeatableAnnotation(resource, OpenApiConstants.DOTNAME_SECURITY_REQUIREMENT, OpenApiConstants.DOTNAME_SECURITY_REQUIREMENTS)
);
for (AnnotationInstance annotation : securityRequirementAnnotations) {
SecurityRequirement requirement = readSecurityRequirement(annotation);
if (requirement != null) {
operation.addSecurityRequirement(requirement);
}
}
// Process @Callback annotations
/////////////////////////////////////////
List callbackAnnotations = JandexUtil.getRepeatableAnnotation(method,
OpenApiConstants.DOTNAME_CALLBACK, OpenApiConstants.DOTNAME_CALLBACKS);
Map callbacks = new LinkedHashMap<>();
for (AnnotationInstance annotation : callbackAnnotations) {
String name = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_NAME);
if (name == null && JandexUtil.isRef(annotation)) {
name = JandexUtil.nameFromRef(annotation);
}
if (name != null) {
callbacks.put(name, readCallback(annotation));
}
if (!callbacks.isEmpty()) {
operation.setCallbacks(callbacks);
}
}
// Process @Server annotations
///////////////////////////////////
List serverAnnotations = JandexUtil.getRepeatableAnnotation(method,
OpenApiConstants.DOTNAME_SERVER, OpenApiConstants.DOTNAME_SERVERS);
if (serverAnnotations.isEmpty()) {
serverAnnotations.addAll(JandexUtil.getRepeatableAnnotation(method.declaringClass(),
OpenApiConstants.DOTNAME_SERVER, OpenApiConstants.DOTNAME_SERVERS));
}
for (AnnotationInstance annotation : serverAnnotations) {
Server server = readServer(annotation);
operation.addServer(server);
}
// Now set the operation on the PathItem as appropriate based on the Http method type
///////////////////////////////////////////
switch (methodType) {
case DELETE:
pathItem.setDELETE(operation);
break;
case GET:
pathItem.setGET(operation);
break;
case HEAD:
pathItem.setHEAD(operation);
break;
case OPTIONS:
pathItem.setOPTIONS(operation);
break;
case PATCH:
pathItem.setPATCH(operation);
break;
case POST:
pathItem.setPOST(operation);
break;
case PUT:
pathItem.setPUT(operation);
break;
case TRACE:
pathItem.setTRACE(operation);
break;
default:
break;
}
}
/**
* Called when a jax-rs method's APIResponse annotations have all been processed but
* no response was actually created for the operation. This method will create a response
* from the method information and add it to the given operation. It will try to do this
* by examining the method's return value and the type of operation (GET, PUT, POST, DELETE).
*
* If there is a return value of some kind (a non-void return type) then the response code
* is assumed to be 200.
*
* If there not a return value (void return type) then either a 201 or 204 is returned,
* depending on the type of request.
*
* TODO generate responses for each checked exception?
* @param method
* @param operation
*/
private void createResponseFromJaxRsMethod(MethodInfo method, Operation operation) {
Type returnType = method.returnType();
Schema schema;
APIResponses responses;
APIResponse response;
ContentImpl content;
if (returnType.kind() == Type.Kind.VOID) {
String code = "204";
if (method.hasAnnotation(OpenApiConstants.DOTNAME_POST)) {
code = "201";
}
responses = ModelUtil.responses(operation);
response = new APIResponseImpl();
responses.addApiResponse(code, response);
} else {
schema = typeToSchema(returnType);
responses = ModelUtil.responses(operation);
response = new APIResponseImpl();
content = new ContentImpl();
String[] produces = this.currentProduces;
if (produces == null || produces.length == 0) {
produces = OpenApiConstants.DEFAULT_PRODUCES;
}
for (String producesType : produces) {
MediaType mt = new MediaTypeImpl();
mt.setSchema(schema);
content.addMediaType(producesType, mt);
}
response.setContent(content);
responses.addApiResponse("200", response);
}
}
/**
* Converts a jandex type to a {@link Schema} model.
* @param type
*/
private Schema typeToSchema(Type type) {
Schema schema = null;
if (type.kind() == Type.Kind.CLASS) {
schema = introspectClassToSchema(type.asClassType(), true);
} else if (type.kind() == Type.Kind.PRIMITIVE) {
schema = OpenApiDataObjectScanner.process(type.asPrimitiveType());
} else {
schema = OpenApiDataObjectScanner.process(index, type);
}
return schema;
}
/**
* Determines where an @Parameter can be found (examples include Query, Path,
* Header, Cookie, etc).
* @param target
*/
private In parameterIn(MethodParameterInfo paramInfo) {
MethodInfo method = paramInfo.method();
short paramPosition = paramInfo.position();
List annotations = JandexUtil.getParameterAnnotations(method, paramPosition);
for (AnnotationInstance annotation : annotations) {
if (annotation.name().equals(OpenApiConstants.DOTNAME_QUERY_PARAM)) {
return In.QUERY;
}
if (annotation.name().equals(OpenApiConstants.DOTNAME_PATH_PARAM)) {
return In.PATH;
}
if (annotation.name().equals(OpenApiConstants.DOTNAME_HEADER_PARAM)) {
return In.HEADER;
}
if (annotation.name().equals(OpenApiConstants.DOTNAME_COOKIE_PARAM)) {
return In.COOKIE;
}
}
return null;
}
/**
* Make a path out of a number of path segments.
* @param segments
*/
protected static String makePath(String ... segments) {
StringBuilder builder = new StringBuilder();
for (String segment : segments) {
if (segment.startsWith("/")) {
segment = segment.substring(1);
}
if (segment.endsWith("/")) {
segment = segment.substring(0, segment.length() - 1);
}
if (segment.isEmpty()) {
continue;
}
builder.append("/");
builder.append(segment);
}
String rval = builder.toString();
if (rval.isEmpty()) {
return "/";
}
return rval;
}
/**
* Reads a OpenAPIDefinition annotation.
* @param openApi
* @param definitionAnno
*/
protected void processDefinition(OpenAPIImpl openApi, AnnotationInstance definitionAnno) {
LOG.debug("Processing an @OpenAPIDefinition annotation.");
openApi.setInfo(readInfo(definitionAnno.value(OpenApiConstants.PROP_INFO)));
openApi.setTags(readTags(definitionAnno.value(OpenApiConstants.PROP_TAGS)));
openApi.setServers(readServers(definitionAnno.value(OpenApiConstants.PROP_SERVERS)));
openApi.setSecurity(readSecurity(definitionAnno.value(OpenApiConstants.PROP_SECURITY)));
openApi.setExternalDocs(readExternalDocs(definitionAnno.value(OpenApiConstants.PROP_EXTERNAL_DOCS)));
openApi.setComponents(readComponents(definitionAnno.value(OpenApiConstants.PROP_COMPONENTS)));
}
/**
* Reads an Info annotation.
* @param infoAnno
*/
private Info readInfo(AnnotationValue infoAnno) {
if (infoAnno == null) {
return null;
}
LOG.debug("Processing an @Info annotation.");
AnnotationInstance nested = infoAnno.asNested();
InfoImpl info = new InfoImpl();
info.setTitle(JandexUtil.stringValue(nested, OpenApiConstants.PROP_TITLE));
info.setDescription(JandexUtil.stringValue(nested, OpenApiConstants.PROP_DESCRIPTION));
info.setTermsOfService(JandexUtil.stringValue(nested, OpenApiConstants.PROP_TERMS_OF_SERVICE));
info.setContact(readContact(nested.value(OpenApiConstants.PROP_CONTACT)));
info.setLicense(readLicense(nested.value(OpenApiConstants.PROP_LICENSE)));
info.setVersion(JandexUtil.stringValue(nested, OpenApiConstants.PROP_VERSION));
return info;
}
/**
* Reads an Contact annotation.
* @param contactAnno
*/
private Contact readContact(AnnotationValue contactAnno) {
if (contactAnno == null) {
return null;
}
LOG.debug("Processing an @Contact annotation.");
AnnotationInstance nested = contactAnno.asNested();
ContactImpl contact = new ContactImpl();
contact.setName(JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME));
contact.setUrl(JandexUtil.stringValue(nested, OpenApiConstants.PROP_URL));
contact.setEmail(JandexUtil.stringValue(nested, OpenApiConstants.PROP_EMAIL));
return contact;
}
/**
* Reads an License annotation.
* @param licenseAnno
*/
private License readLicense(AnnotationValue licenseAnno) {
if (licenseAnno == null) {
return null;
}
LOG.debug("Processing an @License annotation.");
AnnotationInstance nested = licenseAnno.asNested();
LicenseImpl license = new LicenseImpl();
license.setName(JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME));
license.setUrl(JandexUtil.stringValue(nested, OpenApiConstants.PROP_URL));
return license;
}
/**
* Reads any Tag annotations. The annotation
* value is an array of Tag annotations.
* @param tagAnnos
*/
private List readTags(AnnotationValue tagAnnos) {
if (tagAnnos == null) {
return null;
}
LOG.debug("Processing an array of @Tag annotations.");
AnnotationInstance[] nestedArray = tagAnnos.asNestedArray();
List tags = new ArrayList<>();
for (AnnotationInstance tagAnno : nestedArray) {
if (!JandexUtil.isRef(tagAnno)) {
tags.add(readTag(tagAnno));
}
}
return tags;
}
/**
* Reads a single Tag annotation.
* @param tagAnno
*/
private Tag readTag(AnnotationInstance tagAnno) {
if (tagAnno == null) {
return null;
}
LOG.debug("Processing a single @Tag annotation.");
TagImpl tag = new TagImpl();
tag.setName(JandexUtil.stringValue(tagAnno, OpenApiConstants.PROP_NAME));
tag.setDescription(JandexUtil.stringValue(tagAnno, OpenApiConstants.PROP_DESCRIPTION));
tag.setExternalDocs(readExternalDocs(tagAnno.value(OpenApiConstants.PROP_EXTERNAL_DOCS)));
return tag;
}
/**
* Reads any Server annotations. The annotation value is an array of Server annotations.
* @param serverAnnos
*/
private List readServers(AnnotationValue serverAnnos) {
if (serverAnnos == null) {
return null;
}
LOG.debug("Processing an array of @Server annotations.");
AnnotationInstance[] nestedArray = serverAnnos.asNestedArray();
List servers = new ArrayList<>();
for (AnnotationInstance serverAnno : nestedArray) {
servers.add(readServer(serverAnno));
}
return servers;
}
/**
* Reads a single Server annotation.
* @param serverAnno
*/
private Server readServer(AnnotationValue value) {
if (value == null) {
return null;
}
return readServer(value.asNested());
}
/**
* Reads a single Server annotation.
* @param serverAnno
*/
private Server readServer(AnnotationInstance serverAnno) {
if (serverAnno == null) {
return null;
}
LOG.debug("Processing a single @Server annotation.");
ServerImpl server = new ServerImpl();
server.setUrl(JandexUtil.stringValue(serverAnno, OpenApiConstants.PROP_URL));
server.setDescription(JandexUtil.stringValue(serverAnno, OpenApiConstants.PROP_DESCRIPTION));
server.setVariables(readServerVariables(serverAnno.value(OpenApiConstants.PROP_VARIABLES)));
return server;
}
/**
* Reads an array of ServerVariable annotations, returning a new {@link ServerVariables} model. The
* annotation value is an array of ServerVariable annotations.
* @param value
* @return
*/
private ServerVariables readServerVariables(AnnotationValue serverVariableAnnos) {
if (serverVariableAnnos == null) {
return null;
}
LOG.debug("Processing an array of @ServerVariable annotations.");
AnnotationInstance[] nestedArray = serverVariableAnnos.asNestedArray();
ServerVariables variables = new ServerVariablesImpl();
for (AnnotationInstance serverVariableAnno : nestedArray) {
String name = JandexUtil.stringValue(serverVariableAnno, OpenApiConstants.PROP_NAME);
if (name != null) {
variables.addServerVariable(name, readServerVariable(serverVariableAnno));
}
}
return variables;
}
/**
* Reads a single ServerVariable annotation.
* @param serverVariableAnno
*/
private ServerVariable readServerVariable(AnnotationInstance serverVariableAnno) {
if (serverVariableAnno == null) {
return null;
}
LOG.debug("Processing a single @ServerVariable annotation.");
ServerVariable variable = new ServerVariableImpl();
variable.setDescription(JandexUtil.stringValue(serverVariableAnno, OpenApiConstants.PROP_DESCRIPTION));
variable.setEnumeration(JandexUtil.stringListValue(serverVariableAnno, OpenApiConstants.PROP_ENUMERATION));
variable.setDefaultValue(JandexUtil.stringValue(serverVariableAnno, OpenApiConstants.PROP_DEFAULT_VALUE));
return variable;
}
/**
* Reads any SecurityRequirement annotations. The annotation value is an array of
* SecurityRequirement annotations.
* @param value
*/
private List readSecurity(AnnotationValue securityRequirementAnnos) {
if (securityRequirementAnnos == null) {
return null;
}
LOG.debug("Processing an array of @SecurityRequirement annotations.");
AnnotationInstance[] nestedArray = securityRequirementAnnos.asNestedArray();
List requirements = new ArrayList<>();
for (AnnotationInstance requirementAnno : nestedArray) {
SecurityRequirement requirement = readSecurityRequirement(requirementAnno);
if (requirement != null) {
requirements.add(requirement);
}
}
return requirements;
}
/**
* Reads a single SecurityRequirement annotation.
* @param annotation
*/
private SecurityRequirement readSecurityRequirement(AnnotationInstance annotation) {
String name = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_NAME);
if (name != null) {
List scopes = JandexUtil.stringListValue(annotation, OpenApiConstants.PROP_SCOPES);
SecurityRequirement requirement = new SecurityRequirementImpl();
if (scopes == null) {
requirement.addScheme(name);
} else {
requirement.addScheme(name, scopes);
}
return requirement;
}
return null;
}
/**
* Reads an ExternalDocumentation annotation.
* @param externalDocAnno
*/
private ExternalDocumentation readExternalDocs(AnnotationValue externalDocAnno) {
if (externalDocAnno == null) {
return null;
}
LOG.debug("Processing an @ExternalDocumentation annotation.");
AnnotationInstance nested = externalDocAnno.asNested();
ExternalDocumentation externalDoc = new ExternalDocumentationImpl();
externalDoc.setDescription(JandexUtil.stringValue(nested, OpenApiConstants.PROP_DESCRIPTION));
externalDoc.setUrl(JandexUtil.stringValue(nested, OpenApiConstants.PROP_URL));
return externalDoc;
}
/**
* Reads any Components annotations.
* @param componentsAnno
*/
private Components readComponents(AnnotationValue componentsAnno) {
if (componentsAnno == null) {
return null;
}
LOG.debug("Processing an @Components annotation.");
AnnotationInstance nested = componentsAnno.asNested();
Components components = new ComponentsImpl();
// TODO for EVERY item below, handle the case where the annotation is ref-only. then strip the ref path and use the final segment as the name
components.setCallbacks(readCallbacks(nested.value(OpenApiConstants.PROP_CALLBACKS)));
components.setExamples(readExamples(nested.value(OpenApiConstants.PROP_EXAMPLES)));
components.setHeaders(readHeaders(nested.value(OpenApiConstants.PROP_HEADERS)));
components.setLinks(readLinks(nested.value(OpenApiConstants.PROP_LINKS)));
components.setParameters(readParameters(nested.value(OpenApiConstants.PROP_PARAMETERS)));
components.setRequestBodies(readRequestBodies(nested.value(OpenApiConstants.PROP_REQUEST_BODIES)));
components.setResponses(readResponses(nested.value(OpenApiConstants.PROP_RESPONSES)));
components.setSchemas(readSchemas(nested.value(OpenApiConstants.PROP_SCHEMAS)));
components.setSecuritySchemes(readSecuritySchemes(nested.value(OpenApiConstants.PROP_SECURITY_SCHEMES)));
return components;
}
/**
* Reads a map of Callback annotations.
* @param value
*/
private Map readCallbacks(AnnotationValue value) {
if (value == null) {
return null;
}
LOG.debug("Processing a map of @Callback annotations.");
Map map = new LinkedHashMap<>();
AnnotationInstance[] nestedArray = value.asNestedArray();
for (AnnotationInstance nested : nestedArray) {
String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
if (name == null && JandexUtil.isRef(nested)) {
name = JandexUtil.nameFromRef(nested);
}
if (name != null) {
map.put(name, readCallback(nested));
}
}
return map;
}
/**
* Reads a Callback annotation into a model.
* @param annotation
*/
private Callback readCallback(AnnotationInstance annotation) {
if (annotation == null) {
return null;
}
LOG.debug("Processing a single @Callback annotation.");
Callback callback = new CallbackImpl();
callback.setRef(JandexUtil.refValue(annotation, RefType.Callback));
String expression = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_CALLBACK_URL_EXPRESSION);
callback.put(expression, readCallbackOperations(annotation.value(OpenApiConstants.PROP_OPERATIONS)));
return callback;
}
/**
* Reads the CallbackOperation annotations as a PathItem. The annotation value
* in this case is an array of CallbackOperation annotations.
* @param value
*/
private PathItem readCallbackOperations(AnnotationValue value) {
if (value == null) {
return null;
}
LOG.debug("Processing an array of @CallbackOperation annotations.");
AnnotationInstance[] nestedArray = value.asNestedArray();
PathItem pathItem = new PathItemImpl();
for (AnnotationInstance operationAnno : nestedArray) {
String method = JandexUtil.stringValue(operationAnno, OpenApiConstants.PROP_METHOD);
Operation operation = readCallbackOperation(operationAnno);
if (method == null) {
continue;
}
try {
PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(pathItem, method.toUpperCase());
Method mutator = PropertyUtils.getWriteMethod(descriptor);
mutator.invoke(pathItem, operation);
} catch (Exception e) {
LOG.error("Error reading a CallbackOperation annotation.", e);
}
}
return pathItem;
}
/**
* Reads a single CallbackOperation annotation.
* @param operationAnno
* @return
*/
private Operation readCallbackOperation(AnnotationInstance annotation) {
if (annotation == null) {
return null;
}
LOG.debug("Processing a single @CallbackOperation annotation.");
Operation operation = new OperationImpl();
operation.setSummary(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_SUMMARY));
operation.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
operation.setExternalDocs(readExternalDocs(annotation.value(OpenApiConstants.PROP_EXTERNAL_DOCS)));
operation.setParameters(readCallbackOperationParameters(annotation.value(OpenApiConstants.PROP_PARAMETERS)));
operation.setRequestBody(readRequestBody(annotation.value(OpenApiConstants.PROP_REQUEST_BODY)));
operation.setResponses(readCallbackOperationResponses(annotation.value(OpenApiConstants.PROP_RESPONSES)));
operation.setSecurity(readSecurity(annotation.value(OpenApiConstants.PROP_SECURITY)));
operation.setExtensions(readExtensions(annotation.value(OpenApiConstants.PROP_EXTENSIONS)));
return operation;
}
/**
* Reads an array of Parameter annotations into a list.
* @param value
*/
private List readCallbackOperationParameters(AnnotationValue value) {
if (value == null) {
return null;
}
LOG.debug("Processing a list of @Parameter annotations.");
List parameters = new ArrayList<>();
AnnotationInstance[] nestedArray = value.asNestedArray();
for (AnnotationInstance nested : nestedArray) {
parameters.add(readParameter(nested));
}
return parameters;
}
/**
* Reads an array of APIResponse annotations into an {@link APIResponses} model.
* @param value
*/
private APIResponses readCallbackOperationResponses(AnnotationValue value) {
if (value == null) {
return null;
}
LOG.debug("Processing a list of @APIResponse annotations into an APIResponses model.");
APIResponses responses = new APIResponsesImpl();
AnnotationInstance[] nestedArray = value.asNestedArray();
for (AnnotationInstance nested : nestedArray) {
String responseCode = JandexUtil.stringValue(nested, OpenApiConstants.PROP_RESPONSE_CODE);
if (responseCode != null) {
responses.put(responseCode, readResponse(nested));
}
}
return responses;
}
/**
* Reads a map of Example annotations.
* @param value
*/
private Map readExamples(AnnotationValue value) {
if (value == null) {
return null;
}
LOG.debug("Processing a map of @ExampleObject annotations.");
Map map = new LinkedHashMap<>();
AnnotationInstance[] nestedArray = value.asNestedArray();
for (AnnotationInstance nested : nestedArray) {
String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
if (name == null && JandexUtil.isRef(nested)) {
name = JandexUtil.nameFromRef(nested);
}
if (name != null) {
map.put(name, readExample(nested));
}
}
return map;
}
/**
* Reads a Example annotation into a model.
* @param annotation
*/
private Example readExample(AnnotationInstance annotation) {
if (annotation == null) {
return null;
}
LOG.debug("Processing a single @ExampleObject annotation.");
Example example = new ExampleImpl();
example.setSummary(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_SUMMARY));
example.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
example.setValue(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_VALUE));
example.setExternalValue(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_EXTERNAL_VALUE));
example.setRef(JandexUtil.refValue(annotation, RefType.Example));
return example;
}
/**
* Reads a map of Header annotations.
* @param value
*/
private Map readHeaders(AnnotationValue value) {
if (value == null) {
return null;
}
LOG.debug("Processing a map of @Header annotations.");
Map map = new LinkedHashMap<>();
AnnotationInstance[] nestedArray = value.asNestedArray();
for (AnnotationInstance nested : nestedArray) {
String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
if (name == null && JandexUtil.isRef(nested)) {
name = JandexUtil.nameFromRef(nested);
}
if (name != null) {
map.put(name, readHeader(nested));
}
}
return map;
}
/**
* Reads a Header annotation into a model.
* @param annotation
*/
private Header readHeader(AnnotationInstance annotation) {
if (annotation == null) {
return null;
}
LOG.debug("Processing a single @Header annotation.");
Header header = new HeaderImpl();
header.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
header.setSchema(readSchema(annotation.value(OpenApiConstants.PROP_SCHEMA)));
header.setRequired(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_REQUIRED));
header.setDeprecated(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_DEPRECATED));
header.setAllowEmptyValue(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_ALLOW_EMPTY_VALUE));
header.setRef(JandexUtil.refValue(annotation, RefType.Header));
return header;
}
/**
* Reads a map of Link annotations.
* @param value
*/
private Map readLinks(AnnotationValue value) {
if (value == null) {
return null;
}
LOG.debug("Processing a map of @Link annotations.");
Map map = new LinkedHashMap<>();
AnnotationInstance[] nestedArray = value.asNestedArray();
for (AnnotationInstance nested : nestedArray) {
String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
if (name == null && JandexUtil.isRef(nested)) {
name = JandexUtil.nameFromRef(nested);
}
if (name != null) {
map.put(name, readLink(nested));
}
}
return map;
}
/**
* Reads a Link annotation into a model.
* @param annotation
*/
private Link readLink(AnnotationInstance annotation) {
if (annotation == null) {
return null;
}
LOG.debug("Processing a single @Link annotation.");
Link link = new LinkImpl();
link.setOperationRef(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_OPERATION_REF));
link.setOperationId(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_OPERATION_ID));
link.setParameters(readLinkParameters(annotation.value(OpenApiConstants.PROP_PARAMETERS)));
link.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
link.setRequestBody(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_REQUEST_BODY));
link.setServer(readServer(annotation.value(OpenApiConstants.PROP_SERVER)));
link.setRef(JandexUtil.refValue(annotation, RefType.Link));
return link;
}
/**
* Reads an array of LinkParameter annotations into a map.
* @param value
*/
private Map readLinkParameters(AnnotationValue value) {
if (value == null) {
return null;
}
AnnotationInstance[] nestedArray = value.asNestedArray();
Map linkParams = new LinkedHashMap<>();
for (AnnotationInstance annotation : nestedArray) {
String name = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_NAME);
if (name != null) {
String expression = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_EXPRESSION);
linkParams.put(name, expression);
}
}
return linkParams;
}
/**
* Reads a map of Parameter annotations.
* @param value
*/
private Map readParameters(AnnotationValue value) {
if (value == null) {
return null;
}
LOG.debug("Processing a map of @Parameter annotations.");
Map map = new LinkedHashMap<>();
AnnotationInstance[] nestedArray = value.asNestedArray();
for (AnnotationInstance nested : nestedArray) {
String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
if (name == null && JandexUtil.isRef(nested)) {
name = JandexUtil.nameFromRef(nested);
}
if (name != null) {
Parameter parameter = readParameter(nested);
if (parameter != null) {
map.put(name, parameter);
}
}
}
return map;
}
/**
* Reads a Parameter annotation into a model.
* @param annotation
*/
private Parameter readParameter(AnnotationInstance annotation) {
if (annotation == null) {
return null;
}
LOG.debug("Processing a single @Link annotation.");
// Params can be hidden. Skip if that's the case.
Boolean isHidden = JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_HIDDEN);
if (isHidden != null && isHidden == Boolean.TRUE) {
return null;
}
Parameter parameter = new ParameterImpl();
parameter.setName(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_NAME));
parameter.setIn(JandexUtil.enumValue(annotation, OpenApiConstants.PROP_IN, org.eclipse.microprofile.openapi.models.parameters.Parameter.In.class));
parameter.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
parameter.setRequired(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_REQUIRED));
parameter.setDeprecated(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_DEPRECATED));
parameter.setAllowEmptyValue(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_ALLOW_EMPTY_VALUE));
parameter.setStyle(JandexUtil.enumValue(annotation, OpenApiConstants.PROP_STYLE, org.eclipse.microprofile.openapi.models.parameters.Parameter.Style.class));
parameter.setExplode(readExplode(JandexUtil.enumValue(annotation, OpenApiConstants.PROP_EXPLODE, org.eclipse.microprofile.openapi.annotations.enums.Explode.class)));
parameter.setAllowReserved(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_ALLOW_RESERVED));
parameter.setSchema(readSchema(annotation.value(OpenApiConstants.PROP_SCHEMA)));
parameter.setContent(readContent(annotation.value(OpenApiConstants.PROP_CONTENT), ContentDirection.Parameter));
parameter.setExamples(readExamples(annotation.value(OpenApiConstants.PROP_EXAMPLES)));
parameter.setExample(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_EXAMPLE));
parameter.setRef(JandexUtil.refValue(annotation, RefType.Parameter));
return parameter;
}
/**
* Converts from an Explode enum to a true/false/null.
* @param enumValue
*/
private Boolean readExplode(Explode enumValue) {
if (enumValue == Explode.TRUE) {
return Boolean.TRUE;
}
if (enumValue == Explode.FALSE) {
return Boolean.FALSE;
}
return null;
}
/**
* Reads a single Content annotation into a model. The value in this case is an array of
* Content annotations.
* @param value
*/
private Content readContent(AnnotationValue value, ContentDirection direction) {
if (value == null) {
return null;
}
LOG.debug("Processing a single @Content annotation.");
Content content = new ContentImpl();
AnnotationInstance[] nestedArray = value.asNestedArray();
for (AnnotationInstance nested : nestedArray) {
String contentType = JandexUtil.stringValue(nested, OpenApiConstants.PROP_MEDIA_TYPE);
MediaType mediaTypeModel = readMediaType(nested);
if (contentType == null) {
// If the content type is not provided in the @Content annotation, then
// we assume it applies to all the jax-rs method's @Consumes or @Produces
String[] mimeTypes = {};
if (direction == ContentDirection.Input && currentConsumes != null) {
mimeTypes = currentConsumes;
}
if (direction == ContentDirection.Output && currentProduces != null) {
mimeTypes = currentProduces;
}
if (direction == ContentDirection.Parameter) {
mimeTypes = OpenApiConstants.DEFAULT_PARAMETER_MEDIA_TYPES;
}
for (String mimeType : mimeTypes) {
content.addMediaType(mimeType, mediaTypeModel);
}
} else {
content.addMediaType(contentType, mediaTypeModel);
}
}
return content;
}
/**
* Reads a single Content annotation into a {@link MediaType} model.
* @param nested
*/
private MediaType readMediaType(AnnotationInstance annotation) {
if (annotation == null) {
return null;
}
LOG.debug("Processing a single @Content annotation as a MediaType.");
MediaType mediaType = new MediaTypeImpl();
mediaType.setExamples(readExamples(annotation.value(OpenApiConstants.PROP_EXAMPLES)));
mediaType.setSchema(readSchema(annotation.value(OpenApiConstants.PROP_SCHEMA)));
mediaType.setEncoding(readEncodings(annotation.value(OpenApiConstants.PROP_ENCODING)));
return mediaType;
}
/**
* Reads an array of Encoding annotations as a Map.
* @param value
*/
private Map readEncodings(AnnotationValue value) {
if (value == null) {
return null;
}
LOG.debug("Processing a map of @Encoding annotations.");
Map map = new LinkedHashMap<>();
AnnotationInstance[] nestedArray = value.asNestedArray();
for (AnnotationInstance annotation : nestedArray) {
String name = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_NAME);
if (name != null) {
map.put(name, readEncoding(annotation));
}
}
return map;
}
/**
* Reads a single Encoding annotation into a model.
* @param annotation
*/
private Encoding readEncoding(AnnotationInstance annotation) {
if (annotation == null) {
return null;
}
LOG.debug("Processing a single @Encoding annotation.");
Encoding encoding = new EncodingImpl();
encoding.setContentType(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_CONTENT_TYPE));
encoding.setStyle(JandexUtil.enumValue(annotation, OpenApiConstants.PROP_STYLE, org.eclipse.microprofile.openapi.models.media.Encoding.Style.class));
encoding.setExplode(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_EXPLODE));
encoding.setAllowReserved(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_ALLOW_RESERVED));
encoding.setHeaders(readHeaders(annotation.value(OpenApiConstants.PROP_HEADERS)));
return encoding;
}
/**
* Reads a map of RequestBody annotations.
* @param value
*/
private Map readRequestBodies(AnnotationValue value) {
if (value == null) {
return null;
}
LOG.debug("Processing a map of @RequestBody annotations.");
Map map = new LinkedHashMap<>();
AnnotationInstance[] nestedArray = value.asNestedArray();
for (AnnotationInstance nested : nestedArray) {
String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
if (name == null && JandexUtil.isRef(nested)) {
name = JandexUtil.nameFromRef(nested);
}
if (name != null) {
map.put(name, readRequestBody(nested));
}
}
return map;
}
/**
* Reads a RequestBody annotation into a model.
* @param value
*/
private RequestBody readRequestBody(AnnotationValue value) {
if (value == null) {
return null;
}
return readRequestBody(value.asNested());
}
/**
* Reads a RequestBody annotation into a model.
* @param annotation
*/
private RequestBody readRequestBody(AnnotationInstance annotation) {
if (annotation == null) {
return null;
}
LOG.debug("Processing a single @RequestBody annotation.");
RequestBody requestBody = new RequestBodyImpl();
requestBody.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
requestBody.setContent(readContent(annotation.value(OpenApiConstants.PROP_CONTENT), ContentDirection.Input));
requestBody.setRequired(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_REQUIRED));
requestBody.setRef(JandexUtil.refValue(annotation, RefType.RequestBody));
return requestBody;
}
/**
* Reads a map of APIResponse annotations.
* @param value
*/
private Map readResponses(AnnotationValue value) {
if (value == null) {
return null;
}
LOG.debug("Processing a map of @APIResponse annotations.");
Map map = new LinkedHashMap<>();
AnnotationInstance[] nestedArray = value.asNestedArray();
for (AnnotationInstance nested : nestedArray) {
String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
if (name == null && JandexUtil.isRef(nested)) {
name = JandexUtil.nameFromRef(nested);
}
if (name != null) {
map.put(name, readResponse(nested));
}
}
return map;
}
/**
* Reads a APIResponse annotation into a model.
* @param annotation
*/
private APIResponse readResponse(AnnotationInstance annotation) {
if (annotation == null) {
return null;
}
LOG.debug("Processing a single @Response annotation.");
APIResponse response = new APIResponseImpl();
response.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
response.setHeaders(readHeaders(annotation.value(OpenApiConstants.PROP_HEADERS)));
response.setLinks(readLinks(annotation.value(OpenApiConstants.PROP_LINKS)));
response.setContent(readContent(annotation.value(OpenApiConstants.PROP_CONTENT), ContentDirection.Output));
response.setRef(JandexUtil.refValue(annotation, RefType.Response));
return response;
}
/**
* Reads a map of Schema annotations.
* @param value
*/
private Map readSchemas(AnnotationValue value) {
if (value == null) {
return null;
}
LOG.debug("Processing a map of @Schema annotations.");
Map map = new LinkedHashMap<>();
AnnotationInstance[] nestedArray = value.asNestedArray();
for (AnnotationInstance nested : nestedArray) {
String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
if (name == null && JandexUtil.isRef(nested)) {
name = JandexUtil.nameFromRef(nested);
}
if (name != null) {
map.put(name, readSchema(nested));
}
}
return map;
}
/**
* Reads a Schema annotation into a model.
* @param annotation
*/
private Schema readSchema(AnnotationValue value) {
if (value == null) {
return null;
}
return readSchema(value.asNested());
}
/**
* Reads a Schema annotation into a model.
* @param annotation
*/
@SuppressWarnings("unchecked")
private Schema readSchema(AnnotationInstance annotation) {
if (annotation == null) {
return null;
}
LOG.debug("Processing a single @Schema annotation.");
// Schemas can be hidden. Skip if that's the case.
Boolean isHidden = JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_HIDDEN);
if (isHidden != null && isHidden == Boolean.TRUE) {
return null;
}
Schema schema = new SchemaImpl();
schema.setNot(readClassSchema(annotation.value(OpenApiConstants.PROP_NOT), true));
schema.setOneOf(readClassSchemas(annotation.value(OpenApiConstants.PROP_ONE_OF)));
schema.setAnyOf(readClassSchemas(annotation.value(OpenApiConstants.PROP_ANY_OF)));
schema.setAllOf(readClassSchemas(annotation.value(OpenApiConstants.PROP_ALL_OF)));
schema.setTitle(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_TITLE));
schema.setMultipleOf(JandexUtil.bigDecimalValue(annotation, OpenApiConstants.PROP_MULTIPLE_OF));
schema.setMaximum(JandexUtil.bigDecimalValue(annotation, OpenApiConstants.PROP_MAXIMUM));
schema.setExclusiveMaximum(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_EXCLUSIVE_MAXIMUM));
schema.setMinimum(JandexUtil.bigDecimalValue(annotation, OpenApiConstants.PROP_MINIMUM));
schema.setExclusiveMinimum(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_EXCLUSIVE_MINIMUM));
schema.setMaxLength(JandexUtil.intValue(annotation, OpenApiConstants.PROP_MAX_LENGTH));
schema.setMinLength(JandexUtil.intValue(annotation, OpenApiConstants.PROP_MIN_LENGTH));
schema.setPattern(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_PATTERN));
schema.setMaxProperties(JandexUtil.intValue(annotation, OpenApiConstants.PROP_MAX_PROPERTIES));
schema.setMinProperties(JandexUtil.intValue(annotation, OpenApiConstants.PROP_MIN_PROPERTIES));
schema.setRequired(JandexUtil.stringListValue(annotation, OpenApiConstants.PROP_REQUIRED_PROPERTIES));
schema.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
schema.setFormat(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_FORMAT));
schema.setRef(JandexUtil.refValue(annotation, RefType.Schema));
schema.setNullable(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_NULLABLE));
schema.setReadOnly(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_READ_ONLY));
schema.setWriteOnly(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_WRITE_ONLY));
schema.setExample(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_EXAMPLE));
schema.setExternalDocs(readExternalDocs(annotation.value(OpenApiConstants.PROP_EXTERNAL_DOCS)));
schema.setDeprecated(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_DEPRECATED));
schema.setType(JandexUtil.enumValue(annotation, OpenApiConstants.PROP_TYPE, org.eclipse.microprofile.openapi.models.media.Schema.SchemaType.class));
schema.setEnumeration((List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy