com.google.api.server.spi.swagger.SwaggerGenerator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of endpoints-framework Show documentation
Show all versions of endpoints-framework Show documentation
A framework for building RESTful web APIs.
/*
* Copyright 2016 Google Inc. All Rights Reserved.
*
* 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 com.google.api.server.spi.swagger;
import com.google.api.server.spi.Constant;
import com.google.api.server.spi.EndpointMethod;
import com.google.api.server.spi.Strings;
import com.google.api.server.spi.TypeLoader;
import com.google.api.server.spi.config.ApiConfigException;
import com.google.api.server.spi.config.annotationreader.ApiAnnotationIntrospector;
import com.google.api.server.spi.config.model.ApiConfig;
import com.google.api.server.spi.config.model.ApiIssuerAudienceConfig;
import com.google.api.server.spi.config.model.ApiIssuerConfigs;
import com.google.api.server.spi.config.model.ApiIssuerConfigs.IssuerConfig;
import com.google.api.server.spi.config.model.ApiKey;
import com.google.api.server.spi.config.model.ApiMethodConfig;
import com.google.api.server.spi.config.model.ApiParameterConfig;
import com.google.api.server.spi.config.model.FieldType;
import com.google.api.server.spi.config.model.Schema;
import com.google.api.server.spi.config.model.Schema.Field;
import com.google.api.server.spi.config.model.SchemaRepository;
import com.google.api.server.spi.config.validation.ApiConfigValidator;
import com.google.api.server.spi.types.DateAndTime;
import com.google.api.server.spi.types.SimpleDate;
import com.google.common.base.CaseFormat;
import com.google.common.base.Converter;
import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.reflect.TypeToken;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.swagger.models.Info;
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.Scheme;
import io.swagger.models.Swagger;
import io.swagger.models.auth.ApiKeyAuthDefinition;
import io.swagger.models.auth.In;
import io.swagger.models.auth.OAuth2Definition;
import io.swagger.models.auth.SecuritySchemeDefinition;
import io.swagger.models.parameters.BodyParameter;
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.ByteArrayProperty;
import io.swagger.models.properties.DateProperty;
import io.swagger.models.properties.DateTimeProperty;
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;
/**
* Generates a {@link Swagger} object representing a set of {@link ApiConfig} objects.
*/
public class SwaggerGenerator {
private static final String API_KEY = "api_key";
private static final String API_KEY_PARAM = "key";
private static final Converter CONVERTER =
CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.UPPER_CAMEL);
private static final ImmutableMap TYPE_TO_STRING_MAP =
ImmutableMap.builder()
.put(String.class, "string")
.put(Boolean.class, "boolean")
.put(Boolean.TYPE, "boolean")
.put(Integer.class, "integer")
.put(Integer.TYPE, "integer")
.put(Long.class, "integer")
.put(Long.TYPE, "integer")
.put(Float.class, "number")
.put(Float.TYPE, "number")
.put(Double.class, "number")
.put(Double.TYPE, "number")
.put(byte[].class, "string")
.put(SimpleDate.class, "string")
.put(DateAndTime.class, "string")
.put(Date.class, "string")
.build();
private static final ImmutableMap TYPE_TO_FORMAT_MAP =
ImmutableMap.builder()
.put(Integer.class, "int32")
.put(Integer.TYPE, "int32")
.put(Long.class, "int64")
.put(Long.TYPE, "int64")
.put(Float.class, "float")
.put(Float.TYPE, "float")
.put(Double.class, "double")
.put(Double.TYPE, "double")
.put(byte[].class, "byte")
.put(SimpleDate.class, "date")
.put(DateAndTime.class, "date-time")
.put(Date.class, "date-time")
.build();
private static final ImmutableMap FIELD_TYPE_TO_PROPERTY_MAP =
ImmutableMap.builder()
.put(FieldType.BOOLEAN, new BooleanProperty())
.put(FieldType.BYTE_STRING, new ByteArrayProperty())
.put(FieldType.DATE, new DateProperty())
.put(FieldType.DATE_TIME, new DateTimeProperty())
.put(FieldType.DOUBLE, new DoubleProperty())
.put(FieldType.FLOAT, new FloatProperty())
.put(FieldType.INT8, new IntegerProperty())
.put(FieldType.INT16, new IntegerProperty())
.put(FieldType.INT32, new IntegerProperty())
.put(FieldType.INT64, new LongProperty())
.put(FieldType.STRING, new StringProperty())
.build();
private static final Function CONFIG_TO_ROOTLESS_KEY =
new Function() {
@Override
public ApiKey apply(ApiConfig config) {
return new ApiKey(config.getName(), config.getVersion(), null /* root */);
}
};
public Swagger writeSwagger(Iterable configs, boolean writeInternal,
SwaggerContext context) throws ApiConfigException {
try {
return writeSwagger(configs, writeInternal, context,
new SchemaRepository(new TypeLoader(SwaggerGenerator.class.getClassLoader())));
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e);
}
}
public Swagger writeSwagger(Iterable configs, boolean writeInternal,
SwaggerContext context, SchemaRepository repo) throws ApiConfigException {
ImmutableListMultimap configsByKey = FluentIterable.from(configs)
.index(CONFIG_TO_ROOTLESS_KEY);
Swagger swagger = new Swagger()
.produces("application/json")
.consumes("application/json")
.scheme(context.scheme)
.host(context.hostname)
.basePath(context.basePath)
.info(new Info()
.title(context.hostname)
.version(context.docVersion));
for (ApiKey apiKey : configsByKey.keySet()) {
writeApi(apiKey, configsByKey.get(apiKey), swagger, context, writeInternal, repo);
}
return swagger;
}
private void writeApi(ApiKey apiKey, ImmutableList extends ApiConfig> apiConfigs,
Swagger swagger, SwaggerContext context, boolean writeInternal, SchemaRepository repo)
throws ApiConfigException {
ApiConfigValidator validator = new ApiConfigValidator();
for (ApiConfig apiConfig : apiConfigs) {
validator.validate(apiConfig);
for (IssuerConfig issuerConfig : apiConfig.getIssuers().asMap().values()) {
addNonConflictingSecurityDefinition(swagger, issuerConfig);
}
List legacyAudiences = apiConfig.getApiClassConfig().getAudiences();
if (legacyAudiences != null && !legacyAudiences.isEmpty()) {
addNonConflictingSecurityDefinition(swagger, ApiIssuerConfigs.GOOGLE_ID_TOKEN_ISSUER);
addNonConflictingSecurityDefinition(swagger, ApiIssuerConfigs.GOOGLE_ID_TOKEN_ISSUER_ALT);
}
writeApiClass(apiConfig, swagger, context, writeInternal, repo);
}
List schemas = repo.getAllSchemaForApi(apiKey);
if (!schemas.isEmpty()) {
for (Schema schema : schemas) {
swagger.addDefinition(schema.name(), convertToSwaggerSchema(schema));
}
}
}
private void writeApiClass(ApiConfig apiConfig, Swagger swagger, SwaggerContext context,
boolean writeInternal, SchemaRepository repo) throws ApiConfigException {
Map methodConfigs = apiConfig.getApiClassConfig().getMethods();
for (Map.Entry methodConfig : methodConfigs.entrySet()) {
if (!methodConfig.getValue().isIgnored()) {
EndpointMethod endpointMethod = methodConfig.getKey();
ApiMethodConfig config = methodConfig.getValue();
writeApiMethod(
config, endpointMethod, apiConfig, swagger, context, writeInternal, repo);
}
}
}
private void writeApiMethod(ApiMethodConfig methodConfig, EndpointMethod endpointMethod,
ApiConfig apiConfig, Swagger swagger, SwaggerContext context, boolean writeInternal,
SchemaRepository repo) throws ApiConfigException {
Path path = getOrCreatePath(swagger, apiConfig, methodConfig);
Operation operation = new Operation();
operation.setOperationId(getOperationId(apiConfig, methodConfig));
operation.setDescription(methodConfig.getDescription());
Collection pathParameters = methodConfig.getPathParameters();
for (ApiParameterConfig parameterConfig : methodConfig.getParameterConfigs()) {
switch (parameterConfig.getClassification()) {
case API_PARAMETER:
boolean isPathParameter = pathParameters.contains(parameterConfig.getName());
SerializableParameter parameter =
isPathParameter ? new PathParameter() : new QueryParameter();
parameter.setName(parameterConfig.getName());
parameter.setDescription(parameterConfig.getDescription());
boolean required = isPathParameter || (!parameterConfig.getNullable()
&& parameterConfig.getDefaultValue() == null);
if (parameterConfig.isRepeated()) {
TypeToken> t = parameterConfig.getRepeatedItemSerializedType();
parameter.setType("array");
Property p = getSwaggerArrayProperty(t);
if (parameterConfig.isEnum()) { // TODO: Not sure if this is the right check
((StringProperty) p).setEnum(getEnumValues(t));
}
parameter.setItems(p);
} else if (parameterConfig.isEnum()) {
parameter.setType("string");
parameter.setEnum(getEnumValues(parameterConfig.getType()));
parameter.setRequired(required);
} else {
parameter.setType(
TYPE_TO_STRING_MAP.get(parameterConfig.getSchemaBaseType().getType()));
parameter.setFormat(
TYPE_TO_FORMAT_MAP.get(parameterConfig.getSchemaBaseType().getType()));
parameter.setRequired(required);
}
operation.parameter(parameter);
break;
case RESOURCE:
TypeToken> requestType = parameterConfig.getSchemaBaseType();
Schema schema = repo.getOrAdd(requestType, apiConfig);
BodyParameter bodyParameter = new BodyParameter();
bodyParameter.setName("body");
bodyParameter.setSchema(new RefModel(schema.name()));
operation.addParameter(bodyParameter);
break;
case UNKNOWN:
throw new IllegalArgumentException("Unclassifiable parameter type found.");
case INJECTED:
break; // Do nothing, these are synthetic and created by the framework.
}
}
Response response = new Response().description("A successful response");
if (methodConfig.hasResourceInResponse()) {
TypeToken> returnType =
ApiAnnotationIntrospector.getSchemaType(methodConfig.getReturnType(), apiConfig);
Schema schema = repo.getOrAdd(returnType, apiConfig);
response.setSchema(new RefProperty(schema.name()));
}
operation.response(200, response);
writeAudiences(swagger, methodConfig, writeInternal, operation);
if (methodConfig.isApiKeyRequired()) {
operation.addSecurity(API_KEY, ImmutableList.of());
Map definitions = swagger.getSecurityDefinitions();
if (definitions == null || !definitions.containsKey(API_KEY)) {
swagger.securityDefinition(API_KEY, new ApiKeyAuthDefinition(API_KEY_PARAM, In.QUERY));
}
}
path.set(methodConfig.getHttpMethod().toLowerCase(), operation);
}
private void writeAudiences(Swagger swagger, ApiMethodConfig methodConfig, boolean writeInternal,
Operation operation) throws ApiConfigException {
ApiIssuerAudienceConfig issuerAudiences = methodConfig.getIssuerAudiences();
boolean issuerAudiencesIsEmpty = !issuerAudiences.isSpecified() || issuerAudiences.isEmpty();
List legacyAudiences = methodConfig.getAudiences();
boolean legacyAudiencesIsEmpty = legacyAudiences == null || legacyAudiences.isEmpty();
if (issuerAudiencesIsEmpty && legacyAudiencesIsEmpty) {
return;
}
ImmutableMap> audiences = issuerAudiences.asMap();
// For reversability purposes, we can't use helper data structures here. When Swagger reads
// the document back in, it uses primitive data structures.
ImmutableList.Builder>>> xSecurity =
ImmutableList.builder();
if (!issuerAudiencesIsEmpty) {
for (Map.Entry> entry : audiences.entrySet()) {
operation.addSecurity(entry.getKey(), ImmutableList.of());
if (writeInternal) {
xSecurity.add(ImmutableMap.of(entry.getKey(), createAudiences(entry.getValue())));
}
}
}
if (!legacyAudiencesIsEmpty) {
addNonConflictingSecurityDefinition(swagger, ApiIssuerConfigs.GOOGLE_ID_TOKEN_ISSUER);
addNonConflictingSecurityDefinition(swagger, ApiIssuerConfigs.GOOGLE_ID_TOKEN_ISSUER_ALT);
operation.addSecurity(Constant.GOOGLE_ID_TOKEN_NAME, ImmutableList.of());
operation.addSecurity(Constant.GOOGLE_ID_TOKEN_NAME_HTTPS, ImmutableList.of());
if (writeInternal) {
ImmutableMap> legacySwaggerAudiences =
createAudiences(legacyAudiences);
xSecurity.add(
ImmutableMap.of(Constant.GOOGLE_ID_TOKEN_NAME, legacySwaggerAudiences));
xSecurity.add(
ImmutableMap.of(Constant.GOOGLE_ID_TOKEN_NAME_HTTPS, legacySwaggerAudiences));
}
}
if (writeInternal) {
operation.setVendorExtension("x-security", xSecurity.build());
}
}
private Model convertToSwaggerSchema(Schema schema) {
ModelImpl docSchema = new ModelImpl();
Map fields = Maps.newLinkedHashMap();
if (!schema.fields().isEmpty()) {
for (Field f : schema.fields().values()) {
fields.put(f.name(), convertToSwaggerProperty(f));
}
docSchema.setProperties(fields);
}
if (!schema.enumValues().isEmpty()) {
docSchema._enum(schema.enumValues());
}
return docSchema;
}
private Property convertToSwaggerProperty(Field f) {
Property p = FIELD_TYPE_TO_PROPERTY_MAP.get(f.type());
if (p != null) {
return p;
} else if (f.type() == FieldType.OBJECT || f.type() == FieldType.ENUM) {
return new RefProperty(f.schemaReference().get().name());
} else if (f.type() == FieldType.ARRAY) {
return new ArrayProperty(convertToSwaggerProperty(f.arrayItemSchema()));
}
throw new IllegalArgumentException("could not convert field " + f);
}
private static String getOperationId(ApiConfig apiConfig, ApiMethodConfig methodConfig) {
return CONVERTER.convert(apiConfig.getName()) +
CONVERTER.convert(methodConfig.getEndpointMethodName());
}
private static Property getSwaggerArrayProperty(TypeToken> typeToken) {
Class> type = typeToken.getRawType();
if (type == String.class) {
return new StringProperty();
} else if (type == Boolean.class || type == Boolean.TYPE) {
return new BooleanProperty();
} else if (type == Integer.class || type == Integer.TYPE) {
return new IntegerProperty();
} else if (type == Long.class || type == Long.TYPE) {
return new LongProperty();
} else if (type == Float.class || type == Float.TYPE) {
return new FloatProperty();
} else if (type == Double.class || type == Double.TYPE) {
return new DoubleProperty();
} else if (type == byte[].class) {
return new ByteArrayProperty();
} else if (type.isEnum()) {
return new StringProperty();
}
throw new IllegalArgumentException("invalid property type");
}
private Path getOrCreatePath(Swagger swagger, ApiConfig apiConfig, ApiMethodConfig methodConfig) {
String pathStr =
"/" + apiConfig.getName() + "/" + apiConfig.getVersion() + "/" + methodConfig.getPath();
Path path = swagger.getPath(pathStr);
if (path == null) {
path = new Path();
swagger.path(pathStr, path);
}
return path;
}
private static List getEnumValues(TypeToken> t) {
List values = Lists.newArrayList();
for (Object value : t.getRawType().getEnumConstants()) {
values.add(value.toString());
}
return values;
}
public static class SwaggerContext {
private Scheme scheme = Scheme.HTTPS;
private String hostname = "myapi.appspot.com";
private String basePath = "/_ah/api";
private String docVersion = "1.0.0";
public SwaggerContext setApiRoot(String apiRoot) {
try {
URL url = new URL(apiRoot);
hostname = url.getHost();
if (("http".equals(url.getProtocol()) && url.getPort() != 80 && url.getPort() != -1)
|| ("https".equals(url.getProtocol()) && url.getPort() != 443 && url.getPort() != -1)) {
hostname += ":" + url.getPort();
}
basePath = Strings.stripTrailingSlash(url.getPath());
setScheme(url.getProtocol());
return this;
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}
public SwaggerContext setScheme(String scheme) {
this.scheme = "http".equals(scheme) ? Scheme.HTTP : Scheme.HTTPS;
return this;
}
public SwaggerContext setHostname(String hostname) {
this.hostname = hostname;
return this;
}
public SwaggerContext setBasePath(String basePath) {
this.basePath = basePath;
return this;
}
public SwaggerContext setDocVersion(String docVersion) {
this.docVersion = docVersion;
return this;
}
}
private static ImmutableMap> createAudiences(Iterable audiences) {
return ImmutableMap.>of("audiences", ImmutableList.copyOf(audiences));
}
private static SecuritySchemeDefinition toScheme(IssuerConfig issuerConfig) {
OAuth2Definition tokenDef = new OAuth2Definition().implicit("");
tokenDef.setVendorExtension("x-issuer", issuerConfig.getIssuer());
if (!com.google.common.base.Strings.isNullOrEmpty(issuerConfig.getJwksUri())) {
tokenDef.setVendorExtension("x-jwks_uri", issuerConfig.getJwksUri());
}
return tokenDef;
}
private static Map getOrCreateSecurityDefinitionMap(
Swagger swagger) {
Map securityDefinitions = swagger.getSecurityDefinitions();
if (securityDefinitions == null) {
securityDefinitions = new HashMap<>();
swagger.setSecurityDefinitions(securityDefinitions);
}
return securityDefinitions;
}
private static void addNonConflictingSecurityDefinition(
Swagger swagger, IssuerConfig issuerConfig) throws ApiConfigException {
Map securityDefinitions =
getOrCreateSecurityDefinitionMap(swagger);
SecuritySchemeDefinition existingDef = securityDefinitions.get(issuerConfig.getName());
SecuritySchemeDefinition newDef = toScheme(issuerConfig);
if (existingDef != null && !existingDef.equals(newDef)) {
throw new ApiConfigException(
"Multiple conflicting definitions found for issuer " + issuerConfig.getName());
}
swagger.securityDefinition(issuerConfig.getName(), newDef);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy