com.okta.swagger.codegen.AbstractOktaJavaClientCodegen Maven / Gradle / Ivy
/*
* Copyright 2017 Okta
*
* 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.okta.swagger.codegen;
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 com.samskivert.mustache.Mustache;
import io.swagger.codegen.CodegenModel;
import io.swagger.codegen.CodegenOperation;
import io.swagger.codegen.CodegenParameter;
import io.swagger.codegen.CodegenProperty;
import io.swagger.codegen.CodegenType;
import io.swagger.codegen.languages.AbstractJavaCodegen;
import io.swagger.models.HttpMethod;
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.parameters.BodyParameter;
import io.swagger.models.properties.ArrayProperty;
import io.swagger.models.properties.Property;
import io.swagger.models.properties.RefProperty;
import io.swagger.parser.SwaggerException;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;
public abstract class AbstractOktaJavaClientCodegen extends AbstractJavaCodegen {
private final String codeGenName;
public static final String API_FILE_KEY = "apiFile";
private static final String NON_OPTIONAL_PRAMS = "nonOptionalParams";
static final String X_OPENAPI_V3_SCHEMA_REF = "x-openapi-v3-schema-ref";
@SuppressWarnings("hiding")
private final Logger log = LoggerFactory.getLogger(AbstractOktaJavaClientCodegen.class);
protected Map modelTagMap = new HashMap<>();
protected Set enumList = new HashSet<>();
protected Map discriminatorMap = new HashMap<>();
protected Map reverseDiscriminatorMap = new HashMap<>();
protected Set topLevelResources = new HashSet<>();
protected Map rawSwaggerConfig;
public AbstractOktaJavaClientCodegen(String codeGenName, String relativeTemplateDir, String modelPackage) {
super();
this.codeGenName = codeGenName;
this.dateLibrary = "legacy";
outputFolder = "generated-code" + File.separator + codeGenName;
embeddedTemplateDir = templateDir = relativeTemplateDir;
artifactId = "not_used";
this.modelPackage = modelPackage;
// TODO: these are hard coded for now, calling Maven Plugin does NOT set the packages correctly.
invokerPackage = "com.okta.sdk.invoker";
apiPackage = "com.okta.sdk.client";
apiTemplateFiles.clear();
modelTemplateFiles.clear();
}
@Override
public void preprocessSwagger(Swagger swagger) {
// make sure we have the apiFile location
String apiFile = (String) additionalProperties.get(API_FILE_KEY);
if (apiFile == null || apiFile.isEmpty()) {
throw new SwaggerException("'additionalProperties."+API_FILE_KEY +" property is required. This must be " +
"set to the same file that Swagger is using.");
}
try (Reader reader = new InputStreamReader(new FileInputStream(apiFile), StandardCharsets.UTF_8.toString())) {
rawSwaggerConfig = new Yaml().loadAs(reader, Map.class);
} catch (IOException e) {
throw new IllegalStateException("Failed to parse apiFile: "+ apiFile, e);
}
vendorExtensions.put("basePath", swagger.getBasePath());
super.preprocessSwagger(swagger);
tagEnums(swagger);
buildTopLevelResourceList(swagger);
addListModels(swagger);
buildModelTagMap(swagger);
removeListAfterAndLimit(swagger);
moveOperationsToSingleClient(swagger);
handleOktaLinkedOperations(swagger);
buildDiscriminationMap(swagger);
}
/**
* Figure out which models are top level models (directly returned from a endpoint).
* @param swagger The instance of swagger.
*/
protected void buildTopLevelResourceList(Swagger swagger) {
Set resources = new HashSet<>();
// Loop through all of the operations looking for the models that are used as the response and body params
swagger.getPaths().forEach((pathName, path) ->
path.getOperations().forEach(operation -> {
// find all body params
operation.getParameters().forEach(parameter -> {
if (parameter instanceof BodyParameter) {
resources.add(((RefModel) ((BodyParameter)parameter).getSchema()).getSimpleRef());
}
});
// response objects are a more complicated, start with filter for only the 200 responses
operation.getResponses().entrySet().stream()
.filter(entry -> "200".equals(entry.getKey()))
.forEach(entry -> {
// this schema could be a ref or an array property containing a ref (or null)
Property rawSchema = entry.getValue().getSchema();
if (rawSchema != null) {
RefProperty refProperty;
// detect array properties
if (rawSchema instanceof ArrayProperty) {
Property innerProp = ((ArrayProperty) rawSchema).getItems();
if (innerProp instanceof RefProperty) {
refProperty = (RefProperty) innerProp;
} else {
// invalid swagger config file
throw new SwaggerException("Expected 'schema.items.$ref' to exist.");
}
} else if (rawSchema instanceof RefProperty) {
// non array, standard ref property typically in the format of '#/Definitions/MyModel'
refProperty = (RefProperty) rawSchema;
} else {
throw new SwaggerException("Expected 'schema' to be of type 'ArrayProperty' or 'RefProperty'.");
}
// get the simple name 'MyModel' instead of '#/Definitions/MyModel'
resources.add(refProperty.getSimpleRef());
}
});
})
);
// find any children of these resources
swagger.getDefinitions().forEach((name, model) -> {
String parent = (String) model.getVendorExtensions().get("x-okta-parent");
if (parent != null) {
parent = parent.replaceAll(".*/", "");
if (resources.contains(parent)) {
resources.add(parent);
}
}
});
// mark each model with a 'top-level' vendorExtension
resources.stream()
.map(resourceName -> swagger.getDefinitions().get(resourceName))
.forEach(model -> {
model.getVendorExtensions().put("top-level", true);
});
this.topLevelResources = resources;
}
protected void buildDiscriminationMap(Swagger swagger) {
swagger.getDefinitions().forEach((name, model) -> {
ObjectNode discriminatorMapExtention = (ObjectNode) model.getVendorExtensions().get("x-openapi-v3-discriminator");
if (discriminatorMapExtention != null) {
String propertyName = discriminatorMapExtention.get("propertyName").asText();
ObjectNode mapping = (ObjectNode) discriminatorMapExtention.get("mapping");
ObjectMapper mapper = new ObjectMapper();
Map result = mapper.convertValue(mapping, Map.class);
result = result.entrySet().stream()
.collect(Collectors.toMap(e -> e.getValue().substring(e.getValue().lastIndexOf('/')+1), e -> e.getKey()));
result.forEach((key, value) -> {
reverseDiscriminatorMap.put(key, name);
});
discriminatorMap.put(name, new Discriminator(name, propertyName, result));
}
});
}
protected void tagEnums(Swagger swagger) {
swagger.getDefinitions().forEach((name, model) -> {
if (model instanceof ModelImpl && ((ModelImpl) model).getEnum() != null) {
enumList.add(name);
}
});
}
protected void buildModelTagMap(Swagger swagger) {
swagger.getDefinitions().forEach((key, definition) -> {
Object tags = definition.getVendorExtensions().get("x-okta-tags");
if (tags != null) {
// if tags is NOT null, then assume it is an array
if (tags instanceof List) {
if (!((List) tags).isEmpty()) {
String packageName = tagToPackageName(((List) tags).get(0).toString());
addToModelTagMap(key, packageName);
definition.getVendorExtensions().put("x-okta-package", packageName);
}
}
else {
throw new SwaggerException("Model: "+ key + " contains 'x-okta-tags' that is NOT a List.");
}
}
});
}
protected void addToModelTagMap(String modelName, String packageName) {
modelTagMap.put(modelName, packageName);
}
protected String tagToPackageName(String tag) {
return tag.replaceAll("(.)(\\p{Upper})", "$1.$2").toLowerCase(Locale.ENGLISH);
}
public void removeListAfterAndLimit(Swagger swagger) {
swagger.getPaths().forEach((pathName, path) ->
path.getOperations().forEach(operation ->
operation.getParameters().removeIf(param ->
!param.getRequired() &&
("limit".equals(param.getName()) ||
"after".equals(param.getName())))
)
);
}
private void addAllIfNotNull(List destList, List srcList) {
if (srcList != null) {
destList.addAll(srcList);
}
}
private void handleOktaLinkedOperations(Swagger swagger) {
// we want to move any operations defined by the 'x-okta-operations' or 'x-okta-crud' vendor extension to the model
Map modelMap = swagger.getDefinitions().entrySet().stream()
.filter(e -> e.getValue().getVendorExtensions().containsKey("x-okta-operations")
|| e.getValue().getVendorExtensions().containsKey("x-okta-crud"))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
modelMap.forEach((k, model) -> {
List linkNodes = new ArrayList<>();
addAllIfNotNull(linkNodes, (List) model.getVendorExtensions().get("x-okta-operations"));
addAllIfNotNull(linkNodes, (List) model.getVendorExtensions().get("x-okta-crud"));
Map operationMap = new HashMap<>();
linkNodes.forEach(n -> {
String operationId = n.get("operationId").textValue();
// find the swagger path operation
swagger.getPaths().forEach((pathName, path) -> {
Optional> operationEntry = path.getOperationMap().entrySet().stream().filter(e -> e.getValue().getOperationId().equals(operationId)).findFirst();
if (operationEntry.isPresent()) {
Operation operation = operationEntry.get().getValue();
CodegenOperation cgOperation = fromOperation(
pathName,
operationEntry.get().getKey().name().toLowerCase(),
operation,
swagger.getDefinitions(),
swagger);
boolean canLinkMethod = true;
JsonNode aliasNode = n.get("alias");
if (aliasNode != null) {
String alias = aliasNode.textValue();
cgOperation.vendorExtensions.put("alias", alias);
if ("update".equals(alias)) {
model.getVendorExtensions().put("saveable", true);
} else if ("delete".equals(alias)) {
model.getVendorExtensions().put("deletable", true);
cgOperation.vendorExtensions.put("selfDelete", true);
}
else if ("read".equals(alias) || "create".equals(alias)) {
canLinkMethod = false;
}
}
// we do NOT link read or create methods, those need to be on the parent object
if (canLinkMethod) {
// now any params that match the models we need to use the model value directly
// for example if the path contained {id} we would call getId() instead
Map argMap = createArgMap(n);
List cgOtherPathParamList = new ArrayList<>();
List cgParamAllList = new ArrayList<>();
List cgParamModelList = new ArrayList<>();
cgOperation.pathParams.forEach(param -> {
if (argMap.containsKey(param.paramName)) {
String paramName = argMap.get(param.paramName);
cgParamModelList.add(param);
if (model.getProperties() != null) {
CodegenProperty cgProperty = fromProperty(paramName, model.getProperties().get(paramName));
param.vendorExtensions.put("fromModel", cgProperty);
} else {
System.err.println("Model '" + model.getTitle() + "' has no properties");
}
} else {
cgOtherPathParamList.add(param);
}
});
// remove the body param if the body is the object itself
for (Iterator iter = cgOperation.bodyParams.iterator(); iter.hasNext(); ) {
CodegenParameter bodyParam = iter.next();
if (argMap.containsKey(bodyParam.paramName)) {
cgOperation.vendorExtensions.put("bodyIsSelf", true);
iter.remove();
}
}
// do not add the parrent path params to the list (they will be parsed from the href)
SortedSet pathParents = parentPathParams(n);
cgOtherPathParamList.forEach(param -> {
if (!pathParents.contains(param.paramName)) {
cgParamAllList.add(param);
}
});
if (!pathParents.isEmpty()) {
cgOperation.vendorExtensions.put("hasPathParents", true);
cgOperation.vendorExtensions.put("pathParents", pathParents);
}
cgParamAllList.addAll(cgOperation.bodyParams);
cgParamAllList.addAll(cgOperation.queryParams);
// set all params to have more
cgParamAllList.forEach(param -> param.hasMore = true);
// then grab the last one and mark it as the last
if (!cgParamAllList.isEmpty()) {
CodegenParameter param = cgParamAllList.get(cgParamAllList.size() - 1);
param.hasMore = false;
}
cgOperation.vendorExtensions.put("allParams", cgParamAllList);
cgOperation.vendorExtensions.put("fromModelPathParams", cgParamModelList);
addOptionalExtension(cgOperation, cgParamAllList);
operationMap.put(cgOperation.operationId, cgOperation);
// mark the operation as moved so we do NOT add it to the client
operation.getVendorExtensions().put("moved", true);
}
}
});
});
model.getVendorExtensions().put("operations", operationMap.values());
});
}
private Map createArgMap(ObjectNode n) {
Map argMap = new LinkedHashMap<>();
ArrayNode argNodeList = (ArrayNode) n.get("arguments");
if (argNodeList != null) {
for (Iterator argNodeIter = argNodeList.iterator(); argNodeIter.hasNext(); ) {
JsonNode argNode = (JsonNode) argNodeIter.next();
if (argNode.has("src")) {
String src = argNode.get("src").textValue();
String dest = argNode.get("dest").textValue();
if (src != null) {
argMap.put(dest, src); // reverse lookup
}
}
if (argNode.has("self")) {
String dest = argNode.get("dest").textValue();
argMap.put(dest, "this"); // reverse lookup
}
}
}
return argMap;
}
private SortedSet parentPathParams(ObjectNode n) {
SortedSet result = new TreeSet<>();
ArrayNode argNodeList = (ArrayNode) n.get("arguments");
if (argNodeList != null) {
for (JsonNode argNode : argNodeList) {
if (argNode.has("parentSrc")) {
String src = argNode.get("parentSrc").textValue();
String dest = argNode.get("dest").textValue();
if (src != null) {
result.add(dest);
}
}
}
}
return result;
}
private void moveOperationsToSingleClient(Swagger swagger) {
swagger.getPaths().values().forEach(path ->
path.getOperations().forEach(operation ->
operation.setTags(Collections.singletonList("client"))
)
);
}
@Override
public String apiFileFolder() {
return outputFolder + "/" + apiPackage().replace('.', File.separatorChar);
}
@Override
public String modelFileFolder() {
return outputFolder + "/" + modelPackage().replace('.', File.separatorChar);
}
@Override
public CodegenType getTag() {
return CodegenType.CLIENT;
}
@Override
public String getName() {
return codeGenName;
}
@Override
public String getHelp() {
return "Generates a Java client library.";
}
@Override
public String toModelFilename(String name) {
if (modelTagMap.containsKey(name)) {
String tag = modelTagMap.get(name);
return tag.replaceAll("\\.","/") +"/"+ super.toModelFilename(name);
}
return super.toModelFilename(name);
}
@Override
public String toModelImport(String name) {
if (languageSpecificPrimitives.contains(name)) {
throw new IllegalStateException("Cannot import primitives: "+ name);
}
if (modelTagMap.containsKey(name)) {
return modelPackage() +"."+ modelTagMap.get(name) +"."+ name;
}
return super.toModelImport(name);
}
@Override
public CodegenModel fromModel(String name, Model model, Map allDefinitions) {
CodegenModel codegenModel = super.fromModel(name, model, allDefinitions);
// super add these imports, and we don't want that dependency
codegenModel.imports.remove("ApiModel");
if (model.getVendorExtensions().containsKey("x-baseType")) {
String baseType = (String) model.getVendorExtensions().get("x-baseType");
codegenModel.vendorExtensions.put("baseType", toModelName(baseType));
codegenModel.imports.add(toModelName(baseType));
}
Collection operations = (Collection) codegenModel.vendorExtensions.get("operations");
if (operations != null) {
operations.forEach(op -> {
if (op.returnType != null) {
codegenModel.imports.add(op.returnType);
}
if (op.allParams != null) {
op.allParams.stream()
.filter(param -> needToImport(param.dataType))
.forEach(param -> codegenModel.imports.add(param.dataType));
}
});
}
// force alias == false (likely only relevant for Lists, but something changed in swagger 2.2.3 to require this)
codegenModel.isAlias = false;
String parent = (String) model.getVendorExtensions().get("x-okta-parent");
if (StringUtils.isNotEmpty(parent)) {
codegenModel.parent = toApiName(parent.substring(parent.lastIndexOf("/")));
// figure out the resourceClass if this model has a parent
String discriminatorRoot = getRootDiscriminator(name);
if (discriminatorRoot != null) {
model.getVendorExtensions().put("discriminatorRoot", discriminatorRoot);
}
}
// We use '$ref' attributes with siblings, which isn't valid JSON schema (or swagger), so we need process
// additional attributes from the raw schema
Map modelDef = getRawSwaggerDefinition(name);
codegenModel.vars.forEach(codegenProperty -> {
Map rawPropertyMap = getRawSwaggerProperty(modelDef, codegenProperty.baseName);
codegenProperty.isReadOnly = Boolean.TRUE.equals(rawPropertyMap.get("readOnly"));
});
return codegenModel;
}
private List sortOperations(Collection operations) {
return operations.stream()
.sorted(Comparator.comparing(o -> ((CodegenOperation) o).path)
.thenComparing(o -> ((CodegenOperation) o).httpMethod))
.collect(Collectors.toList());
}
private String getRootDiscriminator(String name) {
String result = reverseDiscriminatorMap.get(name);
if (result != null) {
String parentResult = getRootDiscriminator(result);
if (parentResult != null) {
result = parentResult;
}
}
return result;
}
@Override
public Map postProcessOperations(Map objs) {
Map resultMap = super.postProcessOperations(objs);
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy