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

Fix conversion annotations with groovy #1419

Merged
merged 1 commit into from
Feb 16, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
import static io.micronaut.openapi.visitor.SchemaUtils.isIgnoredHeader;
import static io.micronaut.openapi.visitor.SchemaUtils.setOperationOnPathItem;
import static io.micronaut.openapi.visitor.Utils.DEFAULT_MEDIA_TYPES;
import static io.micronaut.openapi.visitor.Utils.getMediaType;

/**
* A {@link io.micronaut.inject.visitor.TypeElementVisitor} the builds the Swagger model from Micronaut controllers at compile time.
Expand Down Expand Up @@ -486,22 +487,22 @@ private void processExtraBodyParameters(VisitorContext context, HttpMethod httpM
if (HttpMethod.permitsRequestBody(httpMethod) && !extraBodyParameters.isEmpty()) {
if (requestBody == null) {
requestBody = new RequestBody();
Content content = new Content();
var content = new Content();
requestBody.setContent(content);
requestBody.setRequired(true);
swaggerOperation.setRequestBody(requestBody);

consumesMediaTypes = CollectionUtils.isEmpty(consumesMediaTypes) ? DEFAULT_MEDIA_TYPES : consumesMediaTypes;
consumesMediaTypes.forEach(mediaType -> {
io.swagger.v3.oas.models.media.MediaType mt = new io.swagger.v3.oas.models.media.MediaType();
var mt = new io.swagger.v3.oas.models.media.MediaType();
var schema = new Schema<>();
schema.setType(TYPE_OBJECT);
mt.setSchema(schema);
content.addMediaType(mediaType.toString(), mt);
});
}
}
if (requestBody != null && !extraBodyParameters.isEmpty()) {
if (requestBody != null && requestBody.getContent() != null && !extraBodyParameters.isEmpty()) {
requestBody.getContent().forEach((mediaTypeName, mediaType) -> {
var schema = mediaType.getSchema();
if (schema == null) {
Expand All @@ -523,7 +524,7 @@ private void processExtraBodyParameters(VisitorContext context, HttpMethod httpM
}
for (TypedElement parameter : extraBodyParameters) {
if (!isRequestBodySchemaSet) {
processBodyParameter(context, openAPI, javadocDescription, MediaType.of(mediaTypeName), schema, parameter);
processBodyParameter(context, openAPI, javadocDescription, getMediaType(mediaTypeName), schema, parameter);
}
if (mediaTypeName.equals(MediaType.MULTIPART_FORM_DATA)) {
if (CollectionUtils.isNotEmpty(schema.getProperties())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ <T> Optional<T> toValue(Map<CharSequence, Object> values, VisitorContext context
try {
return Optional.ofNullable(ConvertUtils.treeToValue(node, type, context));
} catch (JsonProcessingException e) {
warn("Error converting [" + node + "]: to " + type + ": " + e.getMessage(), context);
warn("Error converting [" + node + "]: to " + type + ":\n" + Utils.printStackTrace(e), context);
}
return Optional.empty();
}
Expand Down Expand Up @@ -2277,7 +2277,7 @@ private List<Object> getEnumValues(EnumElement type, String schemaType, String s
try {
enumValues.add(ConvertUtils.normalizeValue(jacksonValue, schemaType, schemaFormat, context));
} catch (JsonProcessingException e) {
warn("Error converting jacksonValue " + jacksonValue + " : to " + type + ": " + e.getMessage(), context, element);
warn("Error converting jacksonValue " + jacksonValue + " : to " + type + ":\n" + Utils.printStackTrace(e), context, element);
enumValues.add(element.getSimpleName());
}
} else {
Expand Down
137 changes: 122 additions & 15 deletions openapi/src/main/java/io/micronaut/openapi/visitor/ConvertUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,17 @@
import io.swagger.v3.oas.annotations.servers.Server;
import io.swagger.v3.oas.annotations.servers.ServerVariable;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.headers.Header;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.Encoding;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;

Expand Down Expand Up @@ -110,7 +117,7 @@ public static <T> T toValue(Map<CharSequence, Object> values, VisitorContext con
try {
return ConvertUtils.treeToValue(node, type, context);
} catch (JsonProcessingException e) {
warn("Error converting [" + node + "]: to " + type + ": " + e.getMessage(), context);
warn("Error converting [" + node + "]: to " + type + ":\n" + Utils.printStackTrace(e), context);
}
return null;
}
Expand Down Expand Up @@ -255,28 +262,22 @@ public static Optional<Object> parseJsonString(Object object) {
*/
public static <T> T treeToValue(JsonNode jn, Class<T> clazz, VisitorContext context) throws JsonProcessingException {

var fixed = false;
T value;
try {
value = CONVERT_JSON_MAPPER.treeToValue(jn, clazz);
} catch (Exception e) {
// fix for problem with groovy. Jackson throw exception with ApiResponse class
if (clazz == ApiResponse.class && jn.has("content")) {
var contentNode = jn.get("content");
((ObjectNode) jn).set("content", null);
value = CONVERT_JSON_MAPPER.treeToValue(jn, clazz);
var result = new Content();
if (contentNode.isArray()) {
for (var content : contentNode) {
processMediaType(result, content);
}
} else {
processMediaType(result, contentNode);
}
((ApiResponse) value).setContent(result);
// maybe exception with groovy
if (context.getLanguage() == VisitorContext.Language.GROOVY) {
value = fixForGroovy(jn, clazz, null);
fixed = true;
} else {
throw e;
}
}
if (!fixed && context.getLanguage() == VisitorContext.Language.GROOVY) {
value = fixForGroovy(jn, clazz, null);
}

if (value == null) {
return null;
Expand Down Expand Up @@ -320,6 +321,112 @@ public static <T> T treeToValue(JsonNode jn, Class<T> clazz, VisitorContext cont
return value;
}

private static <T> Map<String, T> deserMap(String name, JsonNode jn, Class<T> clazz) throws JsonProcessingException {

var mapNode = jn.get(name);
if (mapNode == null) {
return null;
}
((ObjectNode) jn).remove(name);

var iter = mapNode.fieldNames();
var result = new HashMap<String, T>();
while (iter.hasNext()) {
var entryKey = iter.next();
var objectNode = mapNode.get(entryKey);
var object = CONVERT_JSON_MAPPER.treeToValue(objectNode, clazz);
result.put(entryKey, object);
}
return !result.isEmpty() ? result : null;
}

private static <T> T fixForGroovy(JsonNode jn, Class<T> clazz, Exception e) throws JsonProcessingException {

// fix for problem with groovy. Jackson throw exception with Operation class with content and mediaType
// see https://github.com/micronaut-projects/micronaut-openapi/issues/1418
if (clazz == Operation.class) {

var requestBodyNode = jn.get("requestBody");
((ObjectNode) jn).remove("requestBody");
T value = CONVERT_JSON_MAPPER.treeToValue(jn, clazz);
var requestBody = fixContentForGroovy(requestBodyNode, RequestBody.class);
((Operation) value).setRequestBody(requestBody);

var responsesNode = jn.get("responses");
((ObjectNode) jn).remove("responses");
ApiResponses responses = null;
if (responsesNode != null && !responsesNode.isEmpty()) {
responses = new ApiResponses();
var iter = responsesNode.fields();
while (iter.hasNext()) {
var entry = iter.next();
responses.put(entry.getKey(), fixContentForGroovy(entry.getValue(), ApiResponse.class));
}
}
((Operation) value).setResponses(responses);
return value;
} else if (clazz == ApiResponse.class
|| clazz == Header.class
|| clazz == Parameter.class
|| clazz == RequestBody.class) {
return fixContentForGroovy(jn, clazz);
} else {
return CONVERT_JSON_MAPPER.treeToValue(jn, clazz);
}
}

private static <T> T fixContentForGroovy(JsonNode parentNode, Class<T> clazz) throws JsonProcessingException {
if (parentNode == null) {
return null;
}
Map<String, Example> examples = null;
Map<String, Encoding> encoding = null;
Map<String, Object> extensions = null;
Schema<?> schema = null;
JsonNode mediaTypeNode = null;

var contentNode = parentNode.get("content");
if (contentNode != null) {
examples = deserMap("examples", contentNode, Example.class);
encoding = deserMap("encoding", contentNode, Encoding.class);
extensions = deserMap("extensions", contentNode, Object.class);
var schemaNode = contentNode.get("schema");
if (schemaNode != null) {
schema = CONVERT_JSON_MAPPER.treeToValue(schemaNode, Schema.class);
}

mediaTypeNode = contentNode.get("mediaType");
((ObjectNode) contentNode).remove("mediaType");
}
var value = CONVERT_JSON_MAPPER.treeToValue(parentNode, clazz);
Content content = null;
if (value instanceof ApiResponse apiResponse) {
content = apiResponse.getContent();
} else if (value instanceof Header header) {
content = header.getContent();
} else if (value instanceof Parameter parameter) {
content = parameter.getContent();
} else if (value instanceof RequestBody requestBody) {
content = requestBody.getContent();
}

if (content != null) {
var mediaType = content.get("schema");
content.remove("schema");
if (mediaType == null) {
mediaType = new MediaType();
}
var contentType = mediaTypeNode != null ? mediaTypeNode.textValue() : io.micronaut.http.MediaType.APPLICATION_JSON;
content.put(contentType, mediaType);
mediaType.setExamples(examples);
mediaType.setEncoding(encoding);
mediaType.setExtensions(extensions);
mediaType.setSchema(schema);
}

return value;
}

private static void processMediaType(Content result, JsonNode content) throws JsonProcessingException {
var mediaType = content.has("mediaType") ? content.get("mediaType").asText() : io.micronaut.http.MediaType.APPLICATION_JSON;
var mediaTypeObj = CONVERT_JSON_MAPPER.treeToValue(content, MediaType.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ private List<MediaType> mediaTypes(MethodElement element, Class<? extends Annota
return DEFAULT_MEDIA_TYPES;
}
return Arrays.stream(values)
.map(MediaType::of)
.map(Utils::getMediaType)
.distinct()
.toList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ private static List<MediaType> mediaTypes(String... arr) {
return DEFAULT_MEDIA_TYPES;
}
return Arrays.stream(arr)
.map(MediaType::of)
.map(Utils::getMediaType)
.toList();
}

Expand Down
14 changes: 14 additions & 0 deletions openapi/src/main/java/io/micronaut/openapi/visitor/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,20 @@ public final class Utils {
private Utils() {
}

/**
* Get or create MediaType object by name.
*
* @param mediaTypeName name of mediaType
* @return MediaType object
*/
public static MediaType getMediaType(String mediaTypeName) {
try {
return MediaType.of(mediaTypeName);
} catch (Exception e) {
return new MediaType(mediaTypeName);
}
}

/**
* @return An Instance of default {@link PropertyPlaceholderResolver} to resolve placeholders.
*/
Expand Down
Loading
Loading