Skip to content
Open
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
32 changes: 32 additions & 0 deletions .github/workflows/test-java-feign-jackson-headers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Java Feign Jackson headers
on:
push:
paths:
- 'modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java'
- 'modules/openapi-generator/src/main/resources/Java/**'
- 'modules/openapi-generator/src/test/resources/3_1/java/feign-jackson-json-header.yaml'
- 'modules/openapi-generator/src/test/resources/3_1/java/FeignJacksonHeaderRuntimeTest.java'
- '.github/workflows/test-java-feign-jackson-headers.yaml'
pull_request:
paths:
- 'modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java'
- 'modules/openapi-generator/src/main/resources/Java/**'
- 'modules/openapi-generator/src/test/resources/3_1/java/feign-jackson-json-header.yaml'
- 'modules/openapi-generator/src/test/resources/3_1/java/FeignJacksonHeaderRuntimeTest.java'
- '.github/workflows/test-java-feign-jackson-headers.yaml'
jobs:
runtime:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '17'
cache: maven
- run: ./mvnw -B -pl modules/openapi-generator-cli -am -DskipTests package
- run: |
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java --library feign -i modules/openapi-generator/src/test/resources/3_1/java/feign-jackson-json-header.yaml -o /tmp/feign-test --additional-properties=serializationLibrary=jackson --global-property=apis,models,supportingFiles,apiTests=false,modelTests=false
mkdir -p /tmp/feign-test/src/test/java
cp modules/openapi-generator/src/test/resources/3_1/java/FeignJacksonHeaderRuntimeTest.java /tmp/feign-test/src/test/java/
mvn -B -f /tmp/feign-test/pom.xml -Dtest=FeignJacksonHeaderRuntimeTest test
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.openapitools.codegen.languages;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.servers.Server;
Expand Down Expand Up @@ -947,6 +948,17 @@ public void processOpts() {
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) {
super.postProcessOperationsWithModels(objs, allModels);

if (isLibrary(FEIGN) && jackson && objs.getOperations() != null) {
for (CodegenOperation operation : objs.getOperations().getOperation()) {
if (operation.allParams.stream().anyMatch(p -> p.isHeaderParam && p.headerIsJsonMimeType)) {
additionalProperties.put("hasFeignJsonHeader", true);
if (operation.allParams.stream().anyMatch(p -> p.isHeaderParam && p.headerIsJsonMimeType && p.isArray)) {
additionalProperties.put("hasFeignJsonHeaderArrays", true);
}
}
}
}

if (this.getSingleRequestParameter() && (isLibrary(JERSEY2) || isLibrary(JERSEY3) || isLibrary(OKHTTP_GSON) || isLibrary(NATIVE))) {
// loop through operations to set x-group-parameters extension to true if useSingleRequestParameter option is enabled
OperationMap operations = objs.getOperations();
Expand Down Expand Up @@ -1141,6 +1153,13 @@ private static boolean isMultipartType(List<Map<String, String>> consumes) {
return false;
}

@Override
public void processOpenAPI(OpenAPI openAPI) {
super.processOpenAPI(openAPI);
additionalProperties.remove("hasFeignJsonHeader");
additionalProperties.remove("hasFeignJsonHeaderArrays");
}

@Override
public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
super.postProcessModelProperty(model, property);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,103 @@ import feign.Retryer;
{{>generatedAnnotation}}

public class ApiClient {
{{#hasFeignJsonHeader}}
/** Serializes JSON-content headers with the client's Jackson configuration. */
public static class JsonHeaderParamExpander implements feign.Param.Expander {
private final ApiClient client;

public JsonHeaderParamExpander() {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
throw new IllegalStateException("JSON header expander requires a configured ApiClient contract");
}

private JsonHeaderParamExpander(ApiClient client) {
this.client = client;
}

@Override
public String expand(Object value) {
try {
if (value instanceof JsonHeaderIterable) value = ((JsonHeaderIterable) value).value;
return client.objectMapper.copy()
.setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)
.writer()
.with(com.fasterxml.jackson.core.JsonGenerator.Feature.ESCAPE_NON_ASCII)
.writeValueAsString(value);
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
throw new IllegalArgumentException("Failed to serialize JSON header", e);
}
}
}

private static final class JsonHeaderIterable {
private final Object value;
private JsonHeaderIterable(Object value) { this.value = value; }
}

{{#hasFeignJsonHeaderArrays}}
private static final class JsonHeaderInvocationHandler implements java.lang.reflect.InvocationHandler {
private final Object target;
private final java.lang.reflect.InvocationHandler delegate;

private JsonHeaderInvocationHandler(Object target) {
this.target = target;
this.delegate = java.lang.reflect.Proxy.getInvocationHandler(target);
}

@Override
public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) throws Throwable {
if (method.getDeclaringClass() == Object.class && method.getName().equals("equals")) {
Object other = args[0];
if (other == proxy) return true;
if (other != null && java.lang.reflect.Proxy.isProxyClass(other.getClass())) {
java.lang.reflect.InvocationHandler otherHandler = java.lang.reflect.Proxy.getInvocationHandler(other);
if (otherHandler instanceof JsonHeaderInvocationHandler) {
return delegate.equals(((JsonHeaderInvocationHandler) otherHandler).delegate);
}
}
return false;
}
if (args != null) {
args = args.clone();
java.lang.annotation.Annotation[][] annotations = method.getParameterAnnotations();
for (int i = 0; i < args.length; i++) {
if (!(args[i] instanceof Iterable)) continue;
for (java.lang.annotation.Annotation annotation : annotations[i]) {
if (annotation instanceof feign.Param
&& ((feign.Param) annotation).expander() == JsonHeaderParamExpander.class) {
args[i] = new JsonHeaderIterable(args[i]);
break;
}
}
}
}
return delegate.invoke(target, method, args);
}
}
{{/hasFeignJsonHeaderArrays}}

private feign.Contract jsonHeaderContract() {
feign.Contract delegate = new feign.Contract.Default();
return type -> {
java.util.List<feign.MethodMetadata> methods = delegate.parseAndValidateMetadata(type);
for (feign.MethodMetadata method : methods) {
java.util.Map<Integer, feign.Param.Expander> expanders = new java.util.LinkedHashMap<>();
if (method.indexToExpander() != null) expanders.putAll(method.indexToExpander());
java.util.Iterator<java.util.Map.Entry<Integer, Class<? extends feign.Param.Expander>>> entries =
method.indexToExpanderClass().entrySet().iterator();
while (entries.hasNext()) {
java.util.Map.Entry<Integer, Class<? extends feign.Param.Expander>> entry = entries.next();
if (entry.getValue() == JsonHeaderParamExpander.class) {
expanders.put(entry.getKey(), new JsonHeaderParamExpander(this));
entries.remove();
}
}
method.indexToExpander(expanders);
}
return methods;
};
}
{{/hasFeignJsonHeader}}
protected static final Logger log = Logger.getLogger(ApiClient.class.getName());

public interface Api {}
Expand Down Expand Up @@ -89,6 +186,9 @@ public class ApiClient {
{{#feign-hc5}}
.client(APACHE_HTTP5_CLIENT)
{{/feign-hc5}}
{{#hasFeignJsonHeader}}
.contract(jsonHeaderContract())
{{/hasFeignJsonHeader}}
.encoder(new FormEncoder(new JacksonEncoder(objectMapper)))
.decoder(new ApiResponseDecoder(objectMapper))
{{#hasOAuthMethods}}
Expand Down Expand Up @@ -188,6 +288,9 @@ public class ApiClient {

public ApiClient setFeignBuilder(Feign.Builder feignBuilder) {
this.feignBuilder = feignBuilder;
{{#hasFeignJsonHeader}}
this.feignBuilder.contract(jsonHeaderContract());
{{/hasFeignJsonHeader}}
return this;
}

Expand Down Expand Up @@ -255,7 +358,14 @@ public class ApiClient {
* @return The Client
*/
public <T extends Api> T buildClient(Class<T> clientClass) {
{{#hasFeignJsonHeaderArrays}}
T target = feignBuilder.target(clientClass, basePath);
return clientClass.cast(java.lang.reflect.Proxy.newProxyInstance(clientClass.getClassLoader(),
new Class<?>[] { clientClass }, new JsonHeaderInvocationHandler(target)));
{{/hasFeignJsonHeaderArrays}}
{{^hasFeignJsonHeaderArrays}}
return feignBuilder.target(clientClass, basePath);
{{/hasFeignJsonHeaderArrays}}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public interface {{classname}} extends ApiClient.Api {
"{{baseName}}: {{=<% %>=}}{<%paramName%>}<%={{ }}=%>"{{^-last}},
{{/-last}}{{/headerParams}}
})
{{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{nickname}}({{#allParams}}{{^isBodyParam}}{{^isFormParam}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}});
{{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{nickname}}({{#allParams}}{{^isBodyParam}}{{^isFormParam}}{{#jackson}}{{#headerIsJsonMimeType}}@Param(value="{{paramName}}", expander=ApiClient.JsonHeaderParamExpander.class) {{/headerIsJsonMimeType}}{{/jackson}}{{^headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{^jackson}}{{#headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{/jackson}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}});
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved

/**
* {{summary}}
Expand Down Expand Up @@ -83,7 +83,7 @@ public interface {{classname}} extends ApiClient.Api {
"{{baseName}}: {{=<% %>=}}{<%paramName%>}<%={{ }}=%>"{{^-last}},
{{/-last}}{{/headerParams}}
})
ApiResponse<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> {{nickname}}WithHttpInfo({{#allParams}}{{^isBodyParam}}{{^isFormParam}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}});
ApiResponse<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> {{nickname}}WithHttpInfo({{#allParams}}{{^isBodyParam}}{{^isFormParam}}{{#jackson}}{{#headerIsJsonMimeType}}@Param(value="{{paramName}}", expander=ApiClient.JsonHeaderParamExpander.class) {{/headerIsJsonMimeType}}{{/jackson}}{{^headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{^jackson}}{{#headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{/jackson}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}});


{{#hasQueryParams}}
Expand Down Expand Up @@ -128,7 +128,7 @@ public interface {{classname}} extends ApiClient.Api {
"{{baseName}}: {{=<% %>=}}{<%paramName%>}<%={{ }}=%>"{{^-last}},
{{/-last}}{{/headerParams}}
})
{{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{nickname}}({{#allParams}}{{^isQueryParam}}{{^isBodyParam}}{{^isFormParam}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}, {{/isQueryParam}}{{/allParams}}@QueryMap {{operationIdCamelCase}}QueryParams queryParams);
{{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{nickname}}({{#allParams}}{{^isQueryParam}}{{^isBodyParam}}{{^isFormParam}}{{#jackson}}{{#headerIsJsonMimeType}}@Param(value="{{paramName}}", expander=ApiClient.JsonHeaderParamExpander.class) {{/headerIsJsonMimeType}}{{/jackson}}{{^headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{^jackson}}{{#headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{/jackson}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}, {{/isQueryParam}}{{/allParams}}@QueryMap {{operationIdCamelCase}}QueryParams queryParams);

/**
* {{summary}}
Expand Down Expand Up @@ -168,7 +168,7 @@ public interface {{classname}} extends ApiClient.Api {
"{{baseName}}: {{=<% %>=}}{<%paramName%>}<%={{ }}=%>"{{^-last}},
{{/-last}}{{/headerParams}}
})
ApiResponse<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> {{nickname}}WithHttpInfo({{#allParams}}{{^isQueryParam}}{{^isBodyParam}}{{^isFormParam}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}, {{/isQueryParam}}{{/allParams}}@QueryMap {{operationIdCamelCase}}QueryParams queryParams);
ApiResponse<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> {{nickname}}WithHttpInfo({{#allParams}}{{^isQueryParam}}{{^isBodyParam}}{{^isFormParam}}{{#jackson}}{{#headerIsJsonMimeType}}@Param(value="{{paramName}}", expander=ApiClient.JsonHeaderParamExpander.class) {{/headerIsJsonMimeType}}{{/jackson}}{{^headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{^jackson}}{{#headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{/jackson}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}, {{/isQueryParam}}{{/allParams}}@QueryMap {{operationIdCamelCase}}QueryParams queryParams);


/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,39 @@ public class JavaClientCodegenTest {

private static final String JAVA_GENERATOR = "java";

@Test
public void testFeignJacksonJsonHeader() {
final Path output = newTempFolder();
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName(JAVA_GENERATOR)
.setLibrary(FEIGN)
.addAdditionalProperty("serializationLibrary", "jackson")
.setInputSpec("src/test/resources/3_1/java/feign-jackson-json-header.yaml")
.setOutputDir(output.toString());
List<File> files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
validateJavaSourceFiles(files);
Path java = output.resolve("src/main/java/org/openapitools/client");
assertThat(java.resolve("api/DefaultApi.java")).content()
.contains("@Param(value=\"xJsonArg\", expander=ApiClient.JsonHeaderParamExpander.class)",
"@Param(\"xPlain\")");
assertThat(java.resolve("ApiClient.java")).content()
.contains("class JsonHeaderParamExpander", "ESCAPE_NON_ASCII", "writeValueAsString(value)");
}

@Test
public void testFeignJacksonResponseHeaderDoesNotEnableRequestExpander() {
Path output = newTempFolder();
CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName(JAVA_GENERATOR)
.setLibrary(FEIGN)
.addAdditionalProperty("serializationLibrary", "jackson")
.setInputSpec("src/test/resources/3_1/java/feign-jackson-response-header.yaml")
.setOutputDir(output.toString());
new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
assertThat(output.resolve("src/main/java/org/openapitools/client/ApiClient.java"))
.content().doesNotContain("JsonHeaderParamExpander");
}

// This is the kind of information that ideally would be defined and available system-wide
@Getter
enum Library {
Expand Down
Loading
Loading