com.google.api.server.spi.config.jsonwriter.JsonConfigWriter 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.
The newest version!
/*
* 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.config.jsonwriter;
import com.google.api.server.spi.Constant;
import com.google.api.server.spi.EndpointMethod;
import com.google.api.server.spi.ObjectMapperUtil;
import com.google.api.server.spi.TypeLoader;
import com.google.api.server.spi.config.ApiConfigException;
import com.google.api.server.spi.config.ApiConfigWriter;
import com.google.api.server.spi.config.ResourcePropertySchema;
import com.google.api.server.spi.config.ResourceSchema;
import com.google.api.server.spi.config.annotationreader.ApiAnnotationIntrospector;
import com.google.api.server.spi.config.model.ApiAuthConfig;
import com.google.api.server.spi.config.model.ApiCacheControlConfig;
import com.google.api.server.spi.config.model.ApiConfig;
import com.google.api.server.spi.config.model.ApiFrontendLimitsConfig;
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.ApiNamespaceConfig;
import com.google.api.server.spi.config.model.ApiParameterConfig;
import com.google.api.server.spi.config.model.SchemaRepository;
import com.google.api.server.spi.config.model.Types;
import com.google.api.server.spi.config.scope.AuthScopeExpressions;
import com.google.api.server.spi.config.validation.ApiConfigValidator;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.reflect.TypeToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.TypeVariable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nullable;
/**
* Writer of legacy Endpoints API configurations.
*/
public class JsonConfigWriter implements ApiConfigWriter {
/**
* Default deadline to set in lily adapters. Use a default deadline for Swarm APIs that is
* slightly larger than the GAE request processing limits (60 seconds). This value is also used as
* the Harpoon service deadline when we reach BEs outside Google.
*/
private static final double DEFAULT_LILY_DEADLINE = 65.0;
public static final String MAP_SCHEMA_NAME = "JsonMap";
public static final String ANY_SCHEMA_NAME = "_any";
private final TypeLoader typeLoader;
private final ApiConfigValidator validator;
private final ResourceSchemaProvider resourceSchemaProvider = new JacksonResourceSchemaProvider();
public JsonConfigWriter() throws ClassNotFoundException {
this.typeLoader = new TypeLoader(JsonConfigWriter.class.getClassLoader());
this.validator = new ApiConfigValidator(typeLoader, new SchemaRepository(typeLoader));
}
public JsonConfigWriter(TypeLoader typeLoader, ApiConfigValidator validator)
throws ClassNotFoundException {
this.typeLoader = typeLoader;
this.validator = validator;
}
private static final ObjectMapper objectMapper = ObjectMapperUtil.createStandardObjectMapper();
@Override
public Map writeConfig(Iterable extends ApiConfig> configs)
throws ApiConfigException {
Multimap apisByKey = Multimaps.index(configs,
new Function() {
@Override public ApiKey apply(ApiConfig config) {
return config.getApiKey();
}
});
// This *must* retain the order of apisByKey so the lily_java_api BUILD rule has predictable
// output order.
Map results = Maps.newLinkedHashMap();
for (ApiKey apiKey : apisByKey.keySet()) {
Collection extends ApiConfig> apiConfigs = apisByKey.get(apiKey);
validator.validate(apiConfigs);
results.put(apiKey, generateForApi(apiConfigs));
}
return results;
}
@Override
public String getFileExtension() {
return "api";
}
private String generateForApi(Iterable extends ApiConfig> apiConfigs)
throws ApiConfigException {
ObjectNode root = objectMapper.createObjectNode();
// First, generate api-wide configuration options, given any ApiConfig.
ApiConfig apiConfig = Iterables.get(apiConfigs, 0);
convertApi(root, apiConfig);
convertApiAuth(root, apiConfig.getAuthConfig());
convertApiFrontendLimits(root, apiConfig.getFrontendLimitsConfig());
convertApiCacheControl(root, apiConfig.getCacheControlConfig());
convertApiNamespace(root, apiConfig.getNamespaceConfig());
// Next, generate config-specific configuration options,
convertApiMethods(apiConfigs, root);
return toString(root);
}
/** Writes an object node as a string. */
private String toString(ObjectNode node) throws ApiConfigException {
try {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(node);
} catch (IOException e) {
throw new ApiConfigException(e);
}
}
private void setNodePropertyNoConflict(
ObjectNode node, String key, JsonNode value, String errorMessage) {
if (!node.path(key).isMissingNode()) {
throw new IllegalArgumentException(errorMessage);
}
node.set(key, value);
}
private void setNodePropertyNoConflict(ObjectNode node, String key, JsonNode value) {
setNodePropertyNoConflict(node, key, value, "Multiple values for same key '" + key + "'");
}
private void convertApi(ObjectNode root, ApiConfig config) {
root.put("extends", getParentApiFile());
root.put("abstract", config.getIsAbstract());
root.put("root", config.getRoot());
root.put("name", config.getName());
if (config.getCanonicalName() != null) {
root.put("canonicalName", config.getCanonicalName());
}
root.put("version", config.getVersion());
if (config.getTitle() != null) {
root.put("title", config.getTitle());
}
if (config.getDescription() != null) {
root.put("description", config.getDescription());
}
if (config.getDocumentationLink() != null) {
root.put("documentation", config.getDocumentationLink());
}
root.put("defaultVersion", config.getIsDefaultVersion());
ArrayNode discovery = objectMapper.createArrayNode();
discovery.add(config.getIsDiscoverable() ? "PUBLIC" : "OFF");
root.set("discovery", discovery);
ObjectNode adapter = objectMapper.createObjectNode();
adapter.put("bns", config.getBackendRoot());
adapter.put("deadline", DEFAULT_LILY_DEADLINE);
adapter.put("type", "lily");
root.set("adapter", adapter);
}
/**
* Returns the name of the file the config should extend. Subclasses may
* override to use their own api file.
*/
protected String getParentApiFile() {
return "thirdParty.api";
}
/**
* Converts the auth config from the auth annotation. Subclasses may override
* to add additional information to the auth config.
*/
private void convertApiAuth(ObjectNode root, ApiAuthConfig config) {
ObjectNode authConfig = objectMapper.createObjectNode();
authConfig.put("allowCookieAuth", config.getAllowCookieAuth());
List blockedRegions = config.getBlockedRegions();
if (!blockedRegions.isEmpty()) {
ArrayNode blockedRegionsNode = objectMapper.createArrayNode();
for (String region : blockedRegions) {
blockedRegionsNode.add(region);
}
authConfig.set("blockedRegions", blockedRegionsNode);
}
root.set("auth", authConfig);
}
private void convertApiFrontendLimits(ObjectNode root, ApiFrontendLimitsConfig config) {
ObjectNode frontendLimitsConfig = objectMapper.createObjectNode();
frontendLimitsConfig.put("unregisteredUserQps", config.getUnregisteredUserQps());
frontendLimitsConfig.put("unregisteredQps", config.getUnregisteredQps());
frontendLimitsConfig.put("unregisteredDaily", config.getUnregisteredDaily());
convertApiFrontendLimitRules(frontendLimitsConfig, config.getRules());
root.set("frontendLimits", frontendLimitsConfig);
}
private void convertApiCacheControl(ObjectNode root, ApiCacheControlConfig config) {
ObjectNode cacheControlConfig = objectMapper.createObjectNode();
cacheControlConfig.put("type", config.getType());
cacheControlConfig.put("maxAge", config.getMaxAge());
root.set("cacheControl", cacheControlConfig);
}
private void convertApiNamespace(ObjectNode root, ApiNamespaceConfig config) {
if (!config.getOwnerDomain().isEmpty()) {
root.put("ownerDomain", config.getOwnerDomain());
}
if (!config.getOwnerName().isEmpty()) {
root.put("ownerName", config.getOwnerName());
}
if (!config.getPackagePath().isEmpty()) {
root.put("packagePath", config.getPackagePath());
}
}
private void convertApiFrontendLimitRules(ObjectNode frontendLimitsConfig,
List rules) {
ArrayNode rulesConfig = objectMapper.createArrayNode();
for (ApiFrontendLimitsConfig.FrontendLimitsRule rule : rules) {
// TODO: Allow overriding individual rules based on same "match" field?
ObjectNode ruleConfig = objectMapper.createObjectNode();
ruleConfig.put("match", rule.getMatch());
ruleConfig.put("qps", rule.getQps());
ruleConfig.put("userQps", rule.getUserQps());
ruleConfig.put("daily", rule.getDaily());
ruleConfig.put("analyticsId", rule.getAnalyticsId());
rulesConfig.add(ruleConfig);
}
frontendLimitsConfig.set("rules", rulesConfig);
}
private void convertApiMethods(Iterable extends ApiConfig> configs, ObjectNode root)
throws IllegalArgumentException, SecurityException, ApiConfigException {
ObjectNode methodsNode = objectMapper.createObjectNode();
ObjectNode descriptorNode = objectMapper.createObjectNode();
ObjectNode descriptorSchemasNode = objectMapper.createObjectNode();
ObjectNode descriptorMethodsNode = objectMapper.createObjectNode();
descriptorNode.set("schemas", descriptorSchemasNode);
descriptorNode.set("methods", descriptorMethodsNode);
for (ApiConfig config : configs) {
convertApiMethods(methodsNode, descriptorSchemasNode, descriptorMethodsNode, config);
}
root.set("methods", methodsNode);
root.set("descriptor", descriptorNode);
}
private void convertApiMethods(ObjectNode methodsNode, ObjectNode descriptorSchemasNode,
ObjectNode descriptorMethodsNode, ApiConfig apiConfig)
throws IllegalArgumentException, SecurityException, ApiConfigException {
Map methodConfigs = apiConfig.getApiClassConfig().getMethods();
for (Map.Entry methodConfig : methodConfigs.entrySet()) {
if (!methodConfig.getValue().isIgnored()) {
EndpointMethod endpointMethod = methodConfig.getKey();
ApiMethodConfig config = methodConfig.getValue();
convertApiMethod(methodsNode, descriptorSchemasNode, descriptorMethodsNode,
endpointMethod, config, apiConfig);
}
}
}
private void convertApiMethod(ObjectNode methodsNode, ObjectNode descriptorSchemasNode,
ObjectNode descriptorMethodsNode, EndpointMethod endpointMethod, ApiMethodConfig config,
ApiConfig apiConfig)
throws IllegalArgumentException, SecurityException, ApiConfigException {
ObjectNode methodNode = objectMapper.createObjectNode();
setNodePropertyNoConflict(
methodsNode, config.getFullMethodName(), methodNode);
methodNode.put("path", config.getPath());
methodNode.put("description", config.getDescription());
methodNode.put("httpMethod", config.getHttpMethod());
methodNode.set("authLevel", objectMapper.convertValue(config.getAuthLevel(), JsonNode.class));
methodNode.set("scopes", objectMapper.convertValue(
AuthScopeExpressions.encode(config.getScopeExpression()), JsonNode.class));
methodNode.set("audiences", objectMapper.convertValue(config.getAudiences(), JsonNode.class));
methodNode.set("clientIds", objectMapper.convertValue(config.getClientIds(), JsonNode.class));
methodNode.put("rosyMethod", config.getFullJavaName());
ObjectNode descriptorMethodNode = objectMapper.createObjectNode();
setNodePropertyNoConflict(
descriptorMethodsNode, config.getFullJavaName(), descriptorMethodNode);
convertMethodRequest(endpointMethod, methodNode, descriptorSchemasNode, descriptorMethodNode,
config, apiConfig);
convertMethodResponse(endpointMethod, methodNode, descriptorSchemasNode, descriptorMethodNode,
config);
}
private void convertMethodRequest(EndpointMethod endpointMethod, ObjectNode apiMethodNode,
ObjectNode descriptorSchemasNode, ObjectNode descriptorMethodNode, ApiMethodConfig config,
ApiConfig apiConfig)
throws IllegalArgumentException, SecurityException, ApiConfigException {
ObjectNode requestNode = objectMapper.createObjectNode();
convertMethodRequestParameters(endpointMethod, requestNode,
descriptorSchemasNode, descriptorMethodNode, config, apiConfig);
apiMethodNode.set("request", requestNode);
}
private void convertMethodRequestParameters(EndpointMethod endpointMethod, ObjectNode requestNode,
ObjectNode descriptorSchemasNode, ObjectNode descriptorMethodNode, ApiMethodConfig config,
ApiConfig apiConfig)
throws IllegalArgumentException, SecurityException, ApiConfigException {
ObjectNode parametersNode = objectMapper.createObjectNode();
Method method = endpointMethod.getMethod();
List parameterConfigs = config.getParameterConfigs();
for (ApiParameterConfig parameterConfig : parameterConfigs) {
switch (parameterConfig.getClassification()) {
case INJECTED:
// Do nothing.
break;
case API_PARAMETER:
convertSimpleParameter(parameterConfig, parametersNode);
break;
case RESOURCE:
// Inserts resource in.
convertComplexParameter(parameterConfig, method, descriptorSchemasNode,
descriptorMethodNode, apiConfig, parameterConfigs);
break;
case UNKNOWN:
throw new IllegalArgumentException("Unclassifiable parameter type found.");
}
}
// Set API parameter types if needed.
if (parametersNode.size() != 0) {
requestNode.set("parameters", parametersNode);
}
// Sets request body to auto-template if Lily request portion is set..
if (descriptorMethodNode.get("request") != null) {
requestNode.put("body", "autoTemplate(backendRequest)");
requestNode.put("bodyName", "resource");
} else {
requestNode.put("body", "empty");
}
}
/*
* This appends to the API file any type which can go into the methods.request.parameters
* portion of the resulting .api file. This includes parameter types and any type, such as
* enum or lists of simple types, which can be converted into simple types.
*/
private void convertSimpleParameter(ApiParameterConfig config, ObjectNode parametersNode) {
ObjectNode parameterNode = objectMapper.createObjectNode();
TypeToken> type;
if (config.isRepeated()) {
parameterNode.put("repeated", true);
type = config.getRepeatedItemSerializedType();
} else {
type = config.getSchemaBaseType();
}
if (config.isEnum()) {
ObjectNode enumValuesNode = objectMapper.createObjectNode();
for (Object enumConstant : type.getRawType().getEnumConstants()) {
ObjectNode enumNode = objectMapper.createObjectNode();
enumValuesNode.set(enumConstant.toString(), enumNode);
}
parameterNode.set("enum", enumValuesNode);
type = TypeToken.of(String.class);
}
parameterNode.put("type", typeLoader.getParameterTypes().get(type.getRawType()));
parameterNode.put("description", config.getDescription());
parameterNode.put("required", !config.getNullable() && config.getDefaultValue() == null);
// TODO: Try to find a way to move default value interpretation/conversion into the
// general configuration code.
String defaultValue = config.getDefaultValue();
if (defaultValue != null) {
Class> parameterClass = type.getRawType();
try {
objectMapper.convertValue(defaultValue, parameterClass);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(String.format(
"'%s' is not a valid default value for type '%s'", defaultValue, type));
}
parameterNode.put("default", defaultValue);
}
parametersNode.set(config.getName(), parameterNode);
}
/*
* This appends to the API file any type which can NOT go into the methods.request.parameters.
* These will be present as Json schemas inside the Lily descriptor portion of the API file.
*/
private void convertComplexParameter(ApiParameterConfig config, Method method,
ObjectNode descriptorSchemasNode, ObjectNode descriptorMethodNode,
ApiConfig apiConfig, List parameterConfigs) throws ApiConfigException {
TypeToken> type = config.getSchemaBaseType();
ObjectNode requestTypeNode = objectMapper.createObjectNode();
addTypeToNode(descriptorSchemasNode, type, null, requestTypeNode, apiConfig, parameterConfigs);
setNodePropertyNoConflict(descriptorMethodNode, "request", requestTypeNode, "Method "
+ method.getDeclaringClass().getName() + "." + method.getName()
+ " cannot have multiple resource parameters");
}
private void convertMethodResponse(EndpointMethod serviceMethod, ObjectNode methodNode,
ObjectNode descriptorSchemasNode, ObjectNode descriptorMethodNode,
ApiMethodConfig config) throws ApiConfigException {
ObjectNode responseNode = objectMapper.createObjectNode();
methodNode.set("response", responseNode);
if (config.hasResourceInResponse()) {
responseNode.put("body", "autoTemplate(backendResponse)");
// TODO: Get from ApiMethodConfig.
TypeToken> returnType = ApiAnnotationIntrospector.getSchemaType(
serviceMethod.getReturnType(), config.getApiClassConfig().getApiConfig());
descriptorMethodNode.set("response",
convertMethodResponseType(descriptorSchemasNode, returnType, config));
} else {
// Void methods don't generate response sections in the descriptor
responseNode.put("body", "empty");
}
}
/**
* Returns a node with the response object type, wrapping any arrays into a new Collection schema.
*/
private ObjectNode convertMethodResponseType(ObjectNode descriptorSchemasNode,
TypeToken> returnType, ApiMethodConfig config) throws ApiConfigException {
ObjectNode returnTypeNode = objectMapper.createObjectNode();
String responseTypeName =
addTypeToNode(descriptorSchemasNode, returnType, null, returnTypeNode,
config.getApiClassConfig().getApiConfig(), null);
if (Types.isArrayType(returnType)) {
ObjectNode propertiesNode = objectMapper.createObjectNode();
propertiesNode.set("items", returnTypeNode);
ObjectNode arrayWrapperNode = objectMapper.createObjectNode();
arrayWrapperNode.put("id", responseTypeName);
arrayWrapperNode.put("type", "object");
arrayWrapperNode.set("properties", propertiesNode);
descriptorSchemasNode.set(responseTypeName, arrayWrapperNode);
returnTypeNode = objectMapper.createObjectNode();
returnTypeNode.put("$ref", responseTypeName);
}
return returnTypeNode;
}
@VisibleForTesting
String addTypeToSchema(ObjectNode schemasNode, TypeToken> type, ApiConfig apiConfig,
List parameterConfigs) throws ApiConfigException {
return addTypeToSchema(schemasNode, type, null, apiConfig, parameterConfigs);
}
/**
* Adds an arbitrary, non-array type to a given schema config.
*
* @param schemasNode the config to store the generated type schema
* @param type the type from which to generate a schema
* @param enclosingType for bean properties, the enclosing bean type, used for resolving type
* variables
* @return the name of the schema generated from the type
*/
@VisibleForTesting
String addTypeToSchema(
ObjectNode schemasNode, TypeToken> type, TypeToken> enclosingType, ApiConfig apiConfig,
List parameterConfigs) throws ApiConfigException {
if (typeLoader.isSchemaType(type)) {
return typeLoader.getSchemaType(type);
} else if (Types.isObject(type)) {
if (!schemasNode.has(ANY_SCHEMA_NAME)) {
ObjectNode anySchema = objectMapper.createObjectNode();
anySchema.put("id", ANY_SCHEMA_NAME);
anySchema.put("type", "any");
schemasNode.set(ANY_SCHEMA_NAME, anySchema);
}
return ANY_SCHEMA_NAME;
} else if (Types.isMapType(type)) {
if (!schemasNode.has(MAP_SCHEMA_NAME)) {
ObjectNode mapSchema = objectMapper.createObjectNode();
mapSchema.put("id", MAP_SCHEMA_NAME);
mapSchema.put("type", "object");
schemasNode.set(MAP_SCHEMA_NAME, mapSchema);
}
return MAP_SCHEMA_NAME;
}
// If we already have this schema defined, don't define it again!
String typeName = Types.getSimpleName(type, apiConfig.getSerializationConfig());
JsonNode existing = schemasNode.get(typeName);
if (existing != null && existing.isObject()) {
return typeName;
}
ObjectNode schemaNode = objectMapper.createObjectNode();
Class> c = type.getRawType();
if (c.isEnum()) {
schemasNode.set(typeName, schemaNode);
schemaNode.put("id", typeName);
schemaNode.put("type", "string");
ArrayNode enumNode = objectMapper.createArrayNode();
for (Object enumConstant : c.getEnumConstants()) {
enumNode.add(enumConstant.toString());
}
schemaNode.set("enum", enumNode);
} else {
// JavaBean
TypeToken> serializedType = ApiAnnotationIntrospector.getSchemaType(type, apiConfig);
if (!type.equals(serializedType)) {
return addTypeToSchema(schemasNode, serializedType, enclosingType, apiConfig,
parameterConfigs);
} else {
addBeanTypeToSchema(schemasNode, typeName, schemaNode, type, apiConfig, parameterConfigs);
}
}
return typeName;
}
private void addBeanTypeToSchema(ObjectNode schemasNode, String typeName, ObjectNode schemaNode,
TypeToken> type, ApiConfig apiConfig, List parameterConfigs)
throws ApiConfigException {
schemasNode.set(typeName, schemaNode);
schemaNode.put("id", typeName);
schemaNode.put("type", "object");
ObjectNode propertiesNode = objectMapper.createObjectNode();
addBeanProperties(schemasNode, propertiesNode, type, apiConfig, parameterConfigs);
schemaNode.set("properties", propertiesNode);
}
/**
* Iterates over the given JavaBean class and adds the following to the given config object
* (the value of "properties" of a schema object): "<name>": {"type": "<type>"}, where
* "name" is the name of the JavaBean property and "type" the type of its value.
*/
private void addBeanProperties(ObjectNode schemasNode, ObjectNode node, TypeToken> beanType,
ApiConfig apiConfig, List parameterConfigs) throws ApiConfigException {
// CollectionResponse is treated as a bean but it is a parameterized type, too.
ResourceSchema schema = resourceSchemaProvider.getResourceSchema(beanType, apiConfig);
for (Entry entry : schema.getProperties().entrySet()) {
String propertyName = entry.getKey();
ObjectNode propertyNode = objectMapper.createObjectNode();
TypeToken> propertyType = entry.getValue().getType();
if (propertyType != null) {
addTypeToNode(schemasNode, propertyType, beanType, propertyNode,
apiConfig, parameterConfigs);
node.set(propertyName, propertyNode);
}
}
}
/**
* Adds a schema for a type into an output node. For arrays, this generates nested schemas inline
* for however many dimensions are necessary.
*
* @return an appropriate name for the schema if one isn't already assigned
*/
private String addTypeToNode(ObjectNode schemasNode, TypeToken> type,
TypeToken> enclosingType, ObjectNode node, ApiConfig apiConfig,
List parameterConfigs) throws ApiConfigException {
TypeToken> itemType = Types.getArrayItemType(type);
if (typeLoader.isSchemaType(type)) {
String basicTypeName = typeLoader.getSchemaType(type);
addElementTypeToNode(schemasNode, type, basicTypeName, node, apiConfig);
return basicTypeName;
} else if (itemType != null) {
ObjectNode items = objectMapper.createObjectNode();
node.put("type", "array");
node.set(Constant.ITEMS, items);
String itemTypeName = addTypeToNode(schemasNode, itemType, enclosingType, items, apiConfig,
parameterConfigs);
String arraySuffix = "Collection";
StringBuilder sb = new StringBuilder(itemTypeName.length() + arraySuffix.length());
sb.append(itemTypeName).append(arraySuffix);
sb.setCharAt(0, Character.toUpperCase(sb.charAt(0)));
return sb.toString();
} else if (type instanceof TypeVariable) {
throw new IllegalArgumentException(
String.format("Object type %s not supported.", type));
} else {
String typeName = addTypeToSchema(schemasNode, type, enclosingType, apiConfig,
parameterConfigs);
addElementTypeToNode(schemasNode, type, typeName, node, apiConfig);
return typeName;
}
}
/**
* Adds a basic (non-array) type to an output node, assuming the type has a corresponding schema
* in the provided configuration if it will ever have one.
*/
private void addElementTypeToNode(ObjectNode schemasNode, TypeToken> type, String typeName,
ObjectNode node, ApiConfig apiConfig) {
// This check works better than checking schemaTypes in the case of Map
if (schemasNode.has(typeName)) {
node.put("$ref", typeName);
} else {
node.put("type", typeName);
String format = schemaFormatForType(type, apiConfig);
if (format != null) {
node.put("format", format);
}
}
}
// If a type has a serializer installed, resolve down to target type of the serialization chain to
// find the schema format.
@Nullable
private String schemaFormatForType(TypeToken> type, ApiConfig apiConfig) {
TypeToken> serializedType = ApiAnnotationIntrospector.getSchemaType(type, apiConfig);
if (!type.equals(serializedType)) {
return schemaFormatForType(serializedType, apiConfig);
}
return typeLoader.getSchemaFormat(type);
}
}