Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Java/okhttp] Add dynamic operations option #7916

Merged
merged 5 commits into from
Dec 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions bin/configs/java-okhttp-gson-dynamicOperations.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
generatorName: java
outputDir: samples/client/petstore/java/okhttp-gson-dynamicOperations
library: okhttp-gson
inputSpec: modules/openapi-generator/src/test/resources/2_0/petstore-with-fake-endpoints-models-for-testing.yaml
templateDir: modules/openapi-generator/src/main/resources/Java
additionalProperties:
artifactId: petstore-okhttp-gson-dynamicoperations
hideGenerationTimestamp: "true"
dynamicOperations: "true"
1 change: 1 addition & 0 deletions docs/generators/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|disableHtmlEscaping|Disable HTML escaping of JSON strings when using gson (needed to avoid problems with byte[] fields)| |false|
|disallowAdditionalPropertiesIfNotPresent|Specify the behavior when the 'additionalProperties' keyword is not present in the OAS document. If false: the 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications. If true: when the 'additionalProperties' keyword is not present in a schema, the value of 'additionalProperties' is set to false, i.e. no additional properties are allowed. Note: this mode is not compliant with the JSON schema specification. This is the original openapi-generator behavior.This setting is currently ignored for OAS 2.0 documents: 1) When the 'additionalProperties' keyword is not present in a 2.0 schema, additional properties are NOT allowed. 2) Boolean values of the 'additionalProperties' keyword are ignored. It's as if additional properties are NOT allowed.Note: the root cause are issues #1369 and #1371, which must be resolved in the swagger-parser project.|<dl><dt>**false**</dt><dd>The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.</dd><dt>**true**</dt><dd>when the 'additionalProperties' keyword is not present in a schema, the value of 'additionalProperties' is automatically set to false, i.e. no additional properties are allowed. Note: this mode is not compliant with the JSON schema specification. This is the original openapi-generator behavior.</dd></dl>|true|
|discriminatorCaseSensitive|Whether the discriminator value lookup should be case-sensitive or not. This option only works for Java API client| |true|
|dynamicOperations|Generate operations dynamically at runtime from an OAS| |false|
|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true|
|fullJavaUtil|whether to use fully qualified name for classes under java.util. This option only works for Java API client| |false|
|groupId|groupId in generated pom.xml| |org.openapitools|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public class JavaClientCodegen extends AbstractJavaCodegen
public static final String CASE_INSENSITIVE_RESPONSE_HEADERS = "caseInsensitiveResponseHeaders";
public static final String MICROPROFILE_FRAMEWORK = "microprofileFramework";
public static final String USE_ABSTRACTION_FOR_FILES = "useAbstractionForFiles";
public static final String DYNAMIC_OPERATIONS = "dynamicOperations";

public static final String PLAY_24 = "play24";
public static final String PLAY_25 = "play25";
Expand Down Expand Up @@ -107,6 +108,7 @@ public class JavaClientCodegen extends AbstractJavaCodegen
protected boolean useReflectionEqualsHashCode = false;
protected boolean caseInsensitiveResponseHeaders = false;
protected boolean useAbstractionForFiles = false;
protected boolean dynamicOperations = false;
protected String authFolder;
protected String serializationLibrary = null;

Expand Down Expand Up @@ -150,6 +152,7 @@ public JavaClientCodegen() {
cliOptions.add(CliOption.newBoolean(CASE_INSENSITIVE_RESPONSE_HEADERS, "Make API response's headers case-insensitive. Available on " + OKHTTP_GSON + ", " + JERSEY2 + " libraries"));
cliOptions.add(CliOption.newString(MICROPROFILE_FRAMEWORK, "Framework for microprofile. Possible values \"kumuluzee\""));
cliOptions.add(CliOption.newBoolean(USE_ABSTRACTION_FOR_FILES, "Use alternative types instead of java.io.File to allow passing bytes without a file on disk. Available on " + RESTTEMPLATE + " library"));
cliOptions.add(CliOption.newBoolean(DYNAMIC_OPERATIONS, "Generate operations dynamically at runtime from an OAS", this.dynamicOperations));

supportedLibraries.put(JERSEY1, "HTTP client: Jersey client 1.19.x. JSON processing: Jackson 2.9.x. Enable Java6 support using '-DsupportJava6=true'. Enable gzip request encoding using '-DuseGzipFeature=true'. IMPORTANT NOTE: jersey 1.x is no longer actively maintained so please upgrade to 'jersey2' or other HTTP libaries instead.");
supportedLibraries.put(JERSEY2, "HTTP client: Jersey client 2.25.1. JSON processing: Jackson 2.9.x");
Expand Down Expand Up @@ -308,6 +311,11 @@ public void processOpts() {
this.setUseAbstractionForFiles(convertPropertyToBooleanAndWriteBack(USE_ABSTRACTION_FOR_FILES));
}

if (additionalProperties.containsKey(DYNAMIC_OPERATIONS)) {
this.setDynamicOperations(Boolean.valueOf(additionalProperties.get(DYNAMIC_OPERATIONS).toString()));
}
additionalProperties.put(DYNAMIC_OPERATIONS, dynamicOperations);

final String invokerFolder = (sourceFolder + '/' + invokerPackage).replace(".", "/");
final String apiFolder = (sourceFolder + '/' + apiPackage).replace(".", "/");
authFolder = (sourceFolder + '/' + invokerPackage + ".auth").replace(".", "/");
Expand All @@ -324,7 +332,12 @@ public void processOpts() {
supportingFiles.add(new SupportingFile("ApiClient.mustache", invokerFolder, "ApiClient.java"));
supportingFiles.add(new SupportingFile("ServerConfiguration.mustache", invokerFolder, "ServerConfiguration.java"));
supportingFiles.add(new SupportingFile("ServerVariable.mustache", invokerFolder, "ServerVariable.java"));
supportingFiles.add(new SupportingFile("openapi.mustache", "api", "openapi.yaml"));
if (dynamicOperations) {
supportingFiles.add(new SupportingFile("openapi.mustache", projectFolder + "/resources/openapi", "openapi.yaml"));
supportingFiles.add(new SupportingFile("apiOperation.mustache", invokerFolder, "ApiOperation.java"));
} else {
supportingFiles.add(new SupportingFile("openapi.mustache", "api", "openapi.yaml"));
}

if (dateLibrary.equals("java8") && (isLibrary(WEBCLIENT) || isLibrary(VERTX) || isLibrary(RESTTEMPLATE) || isLibrary(RESTEASY) || isLibrary(MICROPROFILE) || isLibrary(JERSEY2))) {
supportingFiles.add(new SupportingFile("JavaTimeFormatter.mustache", invokerFolder, "JavaTimeFormatter.java"));
Expand Down Expand Up @@ -939,6 +952,10 @@ public void setUseAbstractionForFiles(boolean useAbstractionForFiles) {
this.useAbstractionForFiles = useAbstractionForFiles;
}

public void setDynamicOperations(final boolean dynamicOperations) {
this.dynamicOperations = dynamicOperations;
}

/**
* Serialization library.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{{>licenseInfo}}
package {{invokerPackage}};

import io.swagger.v3.oas.models.Operation;

public class ApiOperation {
private final String path;
private final String method;
private final Operation operation;

public ApiOperation(String path, String method, Operation operation) {
this.path = path;
this.method = method;
this.operation = operation;
}

public Operation getOperation() {
return operation;
}

public String getPath() {
return path;
}

public String getMethod() {
return method;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

package {{invokerPackage}};

{{#dynamicOperations}}
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.Parameter.StyleEnum;
import io.swagger.v3.parser.OpenAPIV3Parser;
{{/dynamicOperations}}
import okhttp3.*;
import okhttp3.internal.http.HttpMethod;
import okhttp3.internal.tls.OkHostnameVerifier;
Expand Down Expand Up @@ -86,6 +94,10 @@ public class ApiClient {

private HttpLoggingInterceptor loggingInterceptor;

{{#dynamicOperations}}
private Map<String, ApiOperation> operationLookupMap = new HashMap<>();

{{/dynamicOperations}}
/*
* Basic constructor for ApiClient
*/
Expand Down Expand Up @@ -209,6 +221,11 @@ public class ApiClient {
setUserAgent("{{#httpUserAgent}}{{{.}}}{{/httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/{{{artifactVersion}}}/java{{/httpUserAgent}}");

authentications = new HashMap<String, Authentication>();
{{#dynamicOperations}}

OpenAPI openAPI = new OpenAPIV3Parser().read("openapi/openapi.yaml");
createOperationLookupMap(openAPI);
{{/dynamicOperations}}
}

/**
Expand Down Expand Up @@ -694,6 +711,7 @@ public class ApiClient {
return params;
}

{{^dynamicOperations}}
/**
* Formats the specified collection query parameters to a list of {@code Pair} objects.
*
Expand Down Expand Up @@ -743,6 +761,46 @@ public class ApiClient {

return params;
}
{{/dynamicOperations}}
{{#dynamicOperations}}
public List<Pair> parameterToPairs(Parameter param, Collection value) {
List<Pair> params = new ArrayList<Pair>();

// preconditions
if (param == null || param.getName() == null || param.getName().isEmpty() || value == null) {
return params;
}

// create the params based on the collection format
if (StyleEnum.FORM.equals(param.getStyle()) && Boolean.TRUE.equals(param.getExplode())) {
for (Object item : value) {
params.add(new Pair(param.getName(), escapeString(parameterToString(item))));
}
return params;
}

// collectionFormat is assumed to be "csv" by default
String delimiter = ",";

// escape all delimiters except commas, which are URI reserved
// characters
if (StyleEnum.SPACEDELIMITED.equals(param.getStyle())) {
delimiter = escapeString(" ");
} else if (StyleEnum.PIPEDELIMITED.equals(param.getStyle())) {
delimiter = escapeString("|");
}

StringBuilder sb = new StringBuilder();
for (Object item : value) {
sb.append(delimiter);
sb.append(escapeString(parameterToString(item)));
}

params.add(new Pair(param.getName(), sb.substring(delimiter.length())));

return params;
}
{{/dynamicOperations}}

/**
* Formats the specified collection path parameter to a string value.
Expand Down Expand Up @@ -1467,4 +1525,75 @@ public class ApiClient {
throw new AssertionError(e);
}
}
{{#dynamicOperations}}

public ApiClient createOperationLookupMap(OpenAPI openAPI) {
operationLookupMap = new HashMap<>();
for (Map.Entry<String, PathItem> pathItemEntry : openAPI.getPaths().entrySet()) {
String path = pathItemEntry.getKey();
PathItem pathItem = pathItemEntry.getValue();
addOperationLookupEntry(path, "GET", pathItem.getGet());
addOperationLookupEntry(path, "PUT", pathItem.getPut());
addOperationLookupEntry(path, "POST", pathItem.getPost());
addOperationLookupEntry(path, "DELETE", pathItem.getDelete());
addOperationLookupEntry(path, "OPTIONS", pathItem.getOptions());
addOperationLookupEntry(path, "HEAD", pathItem.getHead());
addOperationLookupEntry(path, "PATCH", pathItem.getPatch());
addOperationLookupEntry(path, "TRACE", pathItem.getTrace());
}
return this;
}

private void addOperationLookupEntry(String path, String method, Operation operation) {
if ( operation != null && operation.getOperationId() != null) {
operationLookupMap.put(
operation.getOperationId(),
new ApiOperation(path, method, operation));
}
}

public Map<String, ApiOperation> getOperationLookupMap() {
return operationLookupMap;
}

public String fillParametersFromOperation(
Operation operation,
Map<String, Object> paramMap,
String path,
List<Pair> queryParams,
List<Pair> collectionQueryParams,
Map<String, String> headerParams,
Map<String, String> cookieParams
) {
for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
Object value = entry.getValue();
for (Parameter param : operation.getParameters()) {
if (entry.getKey().equals(param.getName())) {
switch (param.getIn()) {
case "path":
path = path.replaceAll("\\{" + param.getName() + "\\}", escapeString(value.toString()));
break;
case "query":
if (value instanceof Collection<?>) {
collectionQueryParams.addAll(parameterToPairs(param, (Collection) value));
} else {
queryParams.addAll(parameterToPair(param.getName(), value));
}
break;
case "header":
headerParams.put(param.getName(), parameterToString(value));
break;
case "cookie":
cookieParams.put(param.getName(), parameterToString(value));
break;
default:
throw new IllegalStateException("Unexpected param in: " + param.getIn());
}

}
}
}
return path;
}
{{/dynamicOperations}}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ package {{package}};
import {{invokerPackage}}.ApiCallback;
import {{invokerPackage}}.ApiClient;
import {{invokerPackage}}.ApiException;
{{#dynamicOperations}}
import {{invokerPackage}}.ApiOperation;
{{/dynamicOperations}}
import {{invokerPackage}}.ApiResponse;
import {{invokerPackage}}.Configuration;
import {{invokerPackage}}.Pair;
Expand All @@ -15,6 +18,10 @@ import {{invokerPackage}}.BeanValidationException;
{{/performBeanValidation}}

import com.google.gson.reflect.TypeToken;
{{#dynamicOperations}}
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.parameters.Parameter;
{{/dynamicOperations}}

import java.io.IOException;

Expand Down Expand Up @@ -93,38 +100,63 @@ public class {{classname}} {
Object localVarPostBody = {{#bodyParam}}{{paramName}}{{/bodyParam}}{{^bodyParam}}null{{/bodyParam}};

// create path and map variables
{{^dynamicOperations}}
String localVarPath = "{{{path}}}"{{#pathParams}}
.replaceAll("\\{" + "{{baseName}}" + "\\}", localVarApiClient.escapeString({{#collectionFormat}}localVarApiClient.collectionPathParameterToString("{{{collectionFormat}}}", {{{paramName}}}){{/collectionFormat}}{{^collectionFormat}}{{{paramName}}}.toString(){{/collectionFormat}})){{/pathParams}};
{{/dynamicOperations}}
{{#dynamicOperations}}
ApiOperation apiOperation = localVarApiClient.getOperationLookupMap().get("{{{operationId}}}");
if (apiOperation == null) {
throw new ApiException("Operation not found in OAS");
}
Operation operation = apiOperation.getOperation();
String localVarPath = apiOperation.getPath();
Map<String, Object> paramMap = new HashMap<>();
{{#allParams}}
{{^isFormParam}}
{{^isBodyParam}}
paramMap.put("{{baseName}}", {{paramName}});
{{/isBodyParam}}
{{/isFormParam}}
{{/allParams}}
{{/dynamicOperations}}

{{javaUtilPrefix}}List<Pair> localVarQueryParams = new {{javaUtilPrefix}}ArrayList<Pair>();
{{javaUtilPrefix}}List<Pair> localVarCollectionQueryParams = new {{javaUtilPrefix}}ArrayList<Pair>();
{{javaUtilPrefix}}Map<String, String> localVarHeaderParams = new {{javaUtilPrefix}}HashMap<String, String>();
{{javaUtilPrefix}}Map<String, String> localVarCookieParams = new {{javaUtilPrefix}}HashMap<String, String>();
{{javaUtilPrefix}}Map<String, Object> localVarFormParams = new {{javaUtilPrefix}}HashMap<String, Object>();

{{#formParams}}
if ({{paramName}} != null) {
localVarFormParams.put("{{baseName}}", {{paramName}});
}

{{/formParams}}
{{^dynamicOperations}}
{{#queryParams}}
if ({{paramName}} != null) {
{{#collectionFormat}}localVarCollectionQueryParams.addAll(localVarApiClient.parameterToPairs("{{{collectionFormat}}}", {{/collectionFormat}}{{^collectionFormat}}localVarQueryParams.addAll(localVarApiClient.parameterToPair({{/collectionFormat}}"{{baseName}}", {{paramName}}));
}

{{/queryParams}}
{{javaUtilPrefix}}Map<String, String> localVarHeaderParams = new {{javaUtilPrefix}}HashMap<String, String>();
{{#headerParams}}
if ({{paramName}} != null) {
localVarHeaderParams.put("{{baseName}}", localVarApiClient.parameterToString({{paramName}}));
}

{{/headerParams}}
{{javaUtilPrefix}}Map<String, String> localVarCookieParams = new {{javaUtilPrefix}}HashMap<String, String>();
{{#cookieParams}}
if ({{paramName}} != null) {
localVarCookieParams.put("{{baseName}}", localVarApiClient.parameterToString({{paramName}}));
}

{{/cookieParams}}
{{javaUtilPrefix}}Map<String, Object> localVarFormParams = new {{javaUtilPrefix}}HashMap<String, Object>();
{{#formParams}}
if ({{paramName}} != null) {
localVarFormParams.put("{{baseName}}", {{paramName}});
}
{{/dynamicOperations}}
{{#dynamicOperations}}
localVarPath = localVarApiClient.fillParametersFromOperation(operation, paramMap, localVarPath, localVarQueryParams, localVarCollectionQueryParams, localVarHeaderParams, localVarCookieParams);

{{/formParams}}
{{/dynamicOperations}}
final String[] localVarAccepts = {
{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}
};
Expand All @@ -140,7 +172,7 @@ public class {{classname}} {
localVarHeaderParams.put("Content-Type", localVarContentType);

String[] localVarAuthNames = new String[] { {{#authMethods}}"{{name}}"{{^-last}}, {{/-last}}{{/authMethods}} };
return localVarApiClient.buildCall(localVarPath, "{{httpMethod}}", localVarQueryParams, localVarCollectionQueryParams, localVarPostBody, localVarHeaderParams, localVarCookieParams, localVarFormParams, localVarAuthNames, _callback);
return localVarApiClient.buildCall(localVarPath, {{^dynamicOperations}}"{{httpMethod}}"{{/dynamicOperations}}{{#dynamicOperations}}apiOperation.getMethod(){{/dynamicOperations}}, localVarQueryParams, localVarCollectionQueryParams, localVarPostBody, localVarHeaderParams, localVarCookieParams, localVarFormParams, localVarAuthNames, _callback);
}

{{#isDeprecated}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ dependencies {
{{#threetenbp}}
compile 'org.threeten:threetenbp:1.4.3'
{{/threetenbp}}
{{#dynamicOperations}}
compile 'io.swagger.parser.v3:swagger-parser-v3:2.0.23'
{{/dynamicOperations}}
compile 'javax.annotation:javax.annotation-api:1.3.2'
testCompile 'junit:junit:4.13.1'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ lazy val root = (project in file(".")).
{{#threetenbp}}
"org.threeten" % "threetenbp" % "1.4.3" % "compile",
{{/threetenbp}}
{{#dynamicOperations}}
"io.swagger.parser.v3" % "swagger-parser-v3" "2.0.23" % "compile"
{{/dynamicOperations}}
"io.gsonfire" % "gson-fire" % "1.8.3" % "compile",
"javax.annotation" % "javax.annotation-api" % "1.3.2" % "compile",
"com.google.code.findbugs" % "jsr305" % "3.0.2" % "compile",
Expand Down
Loading