/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.camel.swagger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import io.swagger.jaxrs.config.BeanConfig;
import io.swagger.models.ArrayModel;
import io.swagger.models.Model;
import io.swagger.models.ModelImpl;
import io.swagger.models.Operation;
import io.swagger.models.Path;
import io.swagger.models.RefModel;
import io.swagger.models.Response;
import io.swagger.models.Swagger;
import io.swagger.models.Tag;
import io.swagger.models.parameters.AbstractSerializableParameter;
import io.swagger.models.parameters.BodyParameter;
import io.swagger.models.parameters.FormParameter;
import io.swagger.models.parameters.HeaderParameter;
import io.swagger.models.parameters.Parameter;
import io.swagger.models.parameters.PathParameter;
import io.swagger.models.parameters.QueryParameter;
import io.swagger.models.parameters.SerializableParameter;
import io.swagger.models.properties.ArrayProperty;
import io.swagger.models.properties.BooleanProperty;
import io.swagger.models.properties.DoubleProperty;
import io.swagger.models.properties.FloatProperty;
import io.swagger.models.properties.IntegerProperty;
import io.swagger.models.properties.LongProperty;
import io.swagger.models.properties.Property;
import io.swagger.models.properties.RefProperty;
import io.swagger.models.properties.StringProperty;
import org.apache.camel.model.rest.RestDefinition;
import org.apache.camel.model.rest.RestOperationParamDefinition;
import org.apache.camel.model.rest.RestOperationResponseHeaderDefinition;
import org.apache.camel.model.rest.RestOperationResponseMsgDefinition;
import org.apache.camel.model.rest.RestParamType;
import org.apache.camel.model.rest.VerbDefinition;
import org.apache.camel.spi.ClassResolver;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.ObjectHelper;
/**
* A Camel REST-DSL swagger reader that parse the rest-dsl into a swagger model representation.
* <p/>
* This reader supports the <a href="http://swagger.io/specification/">Swagger Specification 2.0</a>
*/
public class RestSwaggerReader {
/**
* Read the REST-DSL definition's and parse that as a Swagger model representation
*
* @param rests the rest-dsl
* @param route optional route path to filter the rest-dsl to only include from the chose route
* @param config the swagger configuration
* @param classResolver class resolver to use
* @return the swagger model
*/
public Swagger read(List<RestDefinition> rests, String route, BeanConfig config, String camelContextId, ClassResolver classResolver) {
Swagger swagger = new Swagger();
for (RestDefinition rest : rests) {
if (ObjectHelper.isNotEmpty(route) && !route.equals("/")) {
// filter by route
if (!rest.getPath().equals(route)) {
continue;
}
}
parse(swagger, rest, camelContextId, classResolver);
}
// configure before returning
swagger = config.configure(swagger);
return swagger;
}
private void parse(Swagger swagger, RestDefinition rest, String camelContextId, ClassResolver classResolver) {
List<VerbDefinition> verbs = new ArrayList<>(rest.getVerbs());
// must sort the verbs by uri so we group them together when an uri has multiple operations
Collections.sort(verbs, new VerbOrdering());
// we need to group the operations within the same tag, so use the path as default if not configured
String pathAsTag = rest.getTag() != null ? rest.getTag() : FileUtil.stripLeadingSeparator(rest.getPath());
String summary = rest.getDescriptionText();
if (ObjectHelper.isNotEmpty(pathAsTag)) {
// add rest as tag
Tag tag = new Tag();
tag.description(summary);
tag.name(pathAsTag);
swagger.addTag(tag);
}
// gather all types in use
Set<String> types = new LinkedHashSet<>();
for (VerbDefinition verb : verbs) {
// check if the Verb Definition must be excluded from documentation
Boolean apiDocs;
if (verb.getApiDocs() != null) {
apiDocs = verb.getApiDocs();
} else {
// fallback to option on rest
apiDocs = rest.getApiDocs();
}
if (apiDocs != null && !apiDocs) {
continue;
}
String type = verb.getType();
if (ObjectHelper.isNotEmpty(type)) {
if (type.endsWith("[]")) {
type = type.substring(0, type.length() - 2);
}
types.add(type);
}
type = verb.getOutType();
if (ObjectHelper.isNotEmpty(type)) {
if (type.endsWith("[]")) {
type = type.substring(0, type.length() - 2);
}
types.add(type);
}
// there can also be types in response messages
if (verb.getResponseMsgs() != null) {
for (RestOperationResponseMsgDefinition def : verb.getResponseMsgs()) {
type = def.getResponseModel();
if (ObjectHelper.isNotEmpty(type)) {
if (type.endsWith("[]")) {
type = type.substring(0, type.length() - 2);
}
types.add(type);
}
}
}
}
// use annotation scanner to find models (annotated classes)
for (String type : types) {
Class<?> clazz = classResolver.resolveClass(type);
appendModels(clazz, swagger);
}
doParseVerbs(swagger, rest, camelContextId, verbs, pathAsTag);
}
private void doParseVerbs(Swagger swagger, RestDefinition rest, String camelContextId, List<VerbDefinition> verbs, String pathAsTag) {
// used during gathering of apis
List<Path> paths = new ArrayList<>();
String basePath = rest.getPath();
for (VerbDefinition verb : verbs) {
// check if the Verb Definition must be excluded from documentation
Boolean apiDocs;
if (verb.getApiDocs() != null) {
apiDocs = verb.getApiDocs();
} else {
// fallback to option on rest
apiDocs = rest.getApiDocs();
}
if (apiDocs != null && !apiDocs) {
continue;
}
// the method must be in lower case
String method = verb.asVerb().toLowerCase(Locale.US);
// operation path is a key
String opPath = SwaggerHelper.buildUrl(basePath, verb.getUri());
Operation op = new Operation();
if (ObjectHelper.isNotEmpty(pathAsTag)) {
// group in the same tag
op.addTag(pathAsTag);
}
final String routeId = verb.getRouteId();
final String operationId = Optional.ofNullable(rest.getId()).orElse(routeId);
op.operationId(operationId);
// add id as vendor extensions
op.getVendorExtensions().put("x-camelContextId", camelContextId);
op.getVendorExtensions().put("x-routeId", routeId);
Path path = swagger.getPath(opPath);
if (path == null) {
path = new Path();
paths.add(path);
}
path = path.set(method, op);
String consumes = verb.getConsumes() != null ? verb.getConsumes() : rest.getConsumes();
if (consumes != null) {
String[] parts = consumes.split(",");
for (String part : parts) {
op.addConsumes(part);
}
}
String produces = verb.getProduces() != null ? verb.getProduces() : rest.getProduces();
if (produces != null) {
String[] parts = produces.split(",");
for (String part : parts) {
op.addProduces(part);
}
}
if (verb.getDescriptionText() != null) {
op.summary(verb.getDescriptionText());
}
for (RestOperationParamDefinition param : verb.getParams()) {
Parameter parameter = null;
if (param.getType().equals(RestParamType.body)) {
parameter = new BodyParameter();
} else if (param.getType().equals(RestParamType.formData)) {
parameter = new FormParameter();
} else if (param.getType().equals(RestParamType.header)) {
parameter = new HeaderParameter();
} else if (param.getType().equals(RestParamType.path)) {
parameter = new PathParameter();
} else if (param.getType().equals(RestParamType.query)) {
parameter = new QueryParameter();
}
if (parameter != null) {
parameter.setName(param.getName());
parameter.setDescription(param.getDescription());
parameter.setRequired(param.getRequired());
// set type on parameter
if (parameter instanceof SerializableParameter) {
SerializableParameter serializableParameter = (SerializableParameter) parameter;
if (param.getDataType() != null) {
serializableParameter.setType(param.getDataType());
if (param.getDataType().equalsIgnoreCase("array")) {
if (param.getArrayType() != null) {
if (param.getArrayType().equalsIgnoreCase("string")) {
serializableParameter.setItems(new StringProperty());
}
if (param.getArrayType().equalsIgnoreCase("int") || param.getArrayType().equalsIgnoreCase("integer")) {
serializableParameter.setItems(new IntegerProperty());
}
if (param.getArrayType().equalsIgnoreCase("long")) {
serializableParameter.setItems(new LongProperty());
}
if (param.getArrayType().equalsIgnoreCase("float")) {
serializableParameter.setItems(new FloatProperty());
}
if (param.getArrayType().equalsIgnoreCase("double")) {
serializableParameter.setItems(new DoubleProperty());
}
if (param.getArrayType().equalsIgnoreCase("boolean")) {
serializableParameter.setItems(new BooleanProperty());
}
}
}
}
if (param.getCollectionFormat() != null) {
serializableParameter.setCollectionFormat(param.getCollectionFormat().name());
}
if (param.getAllowableValues() != null && !param.getAllowableValues().isEmpty()) {
serializableParameter.setEnum(param.getAllowableValues());
}
}
// set default value on parameter
if (parameter instanceof AbstractSerializableParameter) {
AbstractSerializableParameter qp = (AbstractSerializableParameter) parameter;
if (param.getDefaultValue() != null) {
qp.setDefaultValue(param.getDefaultValue());
}
}
// set schema on body parameter
if (parameter instanceof BodyParameter) {
BodyParameter bp = (BodyParameter) parameter;
if (verb.getType() != null) {
if (verb.getType().endsWith("[]")) {
String typeName = verb.getType();
typeName = typeName.substring(0, typeName.length() - 2);
Property prop = modelTypeAsProperty(typeName, swagger);
if (prop != null) {
ArrayModel arrayModel = new ArrayModel();
arrayModel.setItems(prop);
bp.setSchema(arrayModel);
}
} else {
String ref = modelTypeAsRef(verb.getType(), swagger);
if (ref != null) {
bp.setSchema(new RefModel(ref));
}
}
}
}
op.addParameter(parameter);
}
}
// if we have an out type then set that as response message
if (verb.getOutType() != null) {
Response response = new Response();
Property prop = modelTypeAsProperty(verb.getOutType(), swagger);
response.setSchema(prop);
response.setDescription("Output type");
op.addResponse("200", response);
}
// enrich with configured response messages from the rest-dsl
doParseResponseMessages(swagger, verb, op);
// add path
swagger.path(opPath, path);
}
}
private void doParseResponseMessages(Swagger swagger, VerbDefinition verb, Operation op) {
for (RestOperationResponseMsgDefinition msg : verb.getResponseMsgs()) {
Response response = null;
if (op.getResponses() != null) {
response = op.getResponses().get(msg.getCode());
}
if (response == null) {
response = new Response();
}
if (ObjectHelper.isNotEmpty(msg.getResponseModel())) {
Property prop = modelTypeAsProperty(msg.getResponseModel(), swagger);
response.setSchema(prop);
}
response.setDescription(msg.getMessage());
// add headers
if (msg.getHeaders() != null) {
for (RestOperationResponseHeaderDefinition header : msg.getHeaders()) {
String name = header.getName();
String type = header.getDataType();
if ("string".equals(type)) {
StringProperty sp = new StringProperty();
sp.setName(name);
sp.setDescription(header.getDescription());
if (header.getAllowableValues() != null) {
sp.setEnum(header.getAllowableValues());
}
response.addHeader(name, sp);
} else if ("int".equals(type) || "integer".equals(type)) {
IntegerProperty ip = new IntegerProperty();
ip.setName(name);
ip.setDescription(header.getDescription());
List<Integer> values;
if (!header.getAllowableValues().isEmpty()) {
values = new ArrayList<Integer>();
for (String text : header.getAllowableValues()) {
values.add(Integer.valueOf(text));
}
ip.setEnum(values);
}
response.addHeader(name, ip);
} else if ("long".equals(type)) {
LongProperty lp = new LongProperty();
lp.setName(name);
lp.setDescription(header.getDescription());
List<Long> values;
if (!header.getAllowableValues().isEmpty()) {
values = new ArrayList<Long>();
for (String text : header.getAllowableValues()) {
values.add(Long.valueOf(text));
}
lp.setEnum(values);
}
response.addHeader(name, lp);
} else if ("float".equals(type)) {
FloatProperty lp = new FloatProperty();
lp.setName(name);
lp.setDescription(header.getDescription());
List<Float> values;
if (!header.getAllowableValues().isEmpty()) {
values = new ArrayList<Float>();
for (String text : header.getAllowableValues()) {
values.add(Float.valueOf(text));
}
lp.setEnum(values);
}
response.addHeader(name, lp);
} else if ("double".equals(type)) {
DoubleProperty dp = new DoubleProperty();
dp.setName(name);
dp.setDescription(header.getDescription());
List<Double> values;
if (!header.getAllowableValues().isEmpty()) {
values = new ArrayList<Double>();
for (String text : header.getAllowableValues()) {
values.add(Double.valueOf(text));
}
dp.setEnum(values);
}
response.addHeader(name, dp);
} else if ("boolean".equals(type)) {
BooleanProperty bp = new BooleanProperty();
bp.setName(name);
bp.setDescription(header.getDescription());
response.addHeader(name, bp);
} else if ("array".equals(type)) {
ArrayProperty ap = new ArrayProperty();
ap.setName(name);
ap.setDescription(header.getDescription());
if (header.getArrayType() != null) {
if (header.getArrayType().equalsIgnoreCase("string")) {
ap.setItems(new StringProperty());
}
if (header.getArrayType().equalsIgnoreCase("int") || header.getArrayType().equalsIgnoreCase("integer")) {
ap.setItems(new IntegerProperty());
}
if (header.getArrayType().equalsIgnoreCase("long")) {
ap.setItems(new LongProperty());
}
if (header.getArrayType().equalsIgnoreCase("float")) {
ap.setItems(new FloatProperty());
}
if (header.getArrayType().equalsIgnoreCase("double")) {
ap.setItems(new DoubleProperty());
}
if (header.getArrayType().equalsIgnoreCase("boolean")) {
ap.setItems(new BooleanProperty());
}
}
response.addHeader(name, ap);
}
}
}
op.addResponse(msg.getCode(), response);
}
}
private Model asModel(String typeName, Swagger swagger) {
boolean array = typeName.endsWith("[]");
if (array) {
typeName = typeName.substring(0, typeName.length() - 2);
}
if (swagger.getDefinitions() != null) {
for (Model model : swagger.getDefinitions().values()) {
StringProperty modelType = (StringProperty) model.getVendorExtensions().get("x-className");
if (modelType != null && typeName.equals(modelType.getFormat())) {
return model;
}
}
}
return null;
}
private String modelTypeAsRef(String typeName, Swagger swagger) {
boolean array = typeName.endsWith("[]");
if (array) {
typeName = typeName.substring(0, typeName.length() - 2);
}
Model model = asModel(typeName, swagger);
if (model != null) {
typeName = ((ModelImpl) model).getName();
return typeName;
}
return null;
}
private Property modelTypeAsProperty(String typeName, Swagger swagger) {
boolean array = typeName.endsWith("[]");
if (array) {
typeName = typeName.substring(0, typeName.length() - 2);
}
String ref = modelTypeAsRef(typeName, swagger);
Property prop = ref != null ? new RefProperty(ref) : new StringProperty(typeName);
if (array) {
return new ArrayProperty(prop);
} else {
return prop;
}
}
/**
* If the class is annotated with swagger annotations its parsed into a Swagger model representation
* which is added to swagger
*
* @param clazz the class such as pojo with swagger annotation
* @param swagger the swagger model
*/
private void appendModels(Class clazz, Swagger swagger) {
RestModelConverters converters = new RestModelConverters();
final Map<String, Model> models = converters.readClass(clazz);
for (Map.Entry<String, Model> entry : models.entrySet()) {
// favor keeping any existing model that has the vendor extension in the model
boolean oldExt = false;
if (swagger.getDefinitions() != null && swagger.getDefinitions().get(entry.getKey()) != null) {
Model oldModel = swagger.getDefinitions().get(entry.getKey());
if (oldModel.getVendorExtensions() != null && !oldModel.getVendorExtensions().isEmpty()) {
oldExt = oldModel.getVendorExtensions().get("x-className") != null;
}
}
if (!oldExt) {
swagger.model(entry.getKey(), entry.getValue());
}
}
}
/**
* To sort the rest operations
*/
private static class VerbOrdering implements Comparator<VerbDefinition> {
@Override
public int compare(VerbDefinition a, VerbDefinition b) {
String u1 = "";
if (a.getUri() != null) {
// replace { with _ which comes before a when soring by char
u1 = a.getUri().replace("{", "_");
}
String u2 = "";
if (b.getUri() != null) {
// replace { with _ which comes before a when soring by char
u2 = b.getUri().replace("{", "_");
}
int num = u1.compareTo(u2);
if (num == 0) {
// same uri, so use http method as sorting
num = a.asVerb().compareTo(b.asVerb());
}
return num;
}
}
}