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
151 changes: 146 additions & 5 deletions core/src/main/java/io/apitomy/hub/api/codegen/OpenApi2JaxRs.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public class OpenApi2JaxRs {
static final Map<String, Type<?>> TYPE_CACHE = new HashMap<>();
static final String OPENAPI_OPERATION_ANNOTATION = "org.eclipse.microprofile.openapi.annotations.Operation";
private static final String MEDIA_TYPE_CONSTANT_PREFIX = "MediaType.";
private static final Set<String> HTTP_METHODS = Set.of("get", "put", "post", "delete", "options", "head", "patch", "trace");

protected static ObjectMapper mapper = new ObjectMapper();
protected static Charset utf8 = StandardCharsets.UTF_8;
Expand All @@ -123,6 +124,7 @@ public class OpenApi2JaxRs {
protected transient Document document;
protected JaxRsProjectSettings settings;
protected boolean updateOnly;
private Map<String, String> requestBodyTypesByOperationId = Map.of();

private GenerationConfig config;

Expand Down Expand Up @@ -381,6 +383,7 @@ protected CodegenInfo getInfoFromApiDoc() throws IOException {
document = Library.transformDocument(document, ModelType.OPENAPI31);

// Pre-process the document
requestBodyTypesByOperationId = computeRequestBodyTypes(document);
document = preProcess(document);

// Figure out the breakdown of the interfaces.
Expand Down Expand Up @@ -451,7 +454,7 @@ protected JavaClassSource generateApplicationClassSource(String topLevelPackage,
* @param document
*/
protected Document preProcess(Document document) {
DocumentPreProcessor preprocessor = new DocumentPreProcessor();
DocumentPreProcessor preprocessor = new DocumentPreProcessor(this.settings, document);
preprocessor.process(document);

if (Boolean.FALSE) {
Expand Down Expand Up @@ -642,18 +645,26 @@ protected String generateJavaInterface(CodegenInfo info, CodegenJavaInterface in
.orElseGet(Stream::empty)
.forEach(arg -> {
String methodArgName = paramNameToJavaArgName(arg.getName());
String defaultParamType = Object.class.getName();
Type<?> paramType = null;

if (arg.getIn().equals("body")) {
// Swagger 2.0?
defaultParamType = InputStream.class.getName();
String replacementType = requestBodyTypesByOperationId.get(methodInfo.getOperationId());
if (replacementType != null) {
paramType = parseType(replacementType);
}
} else if (arg.getIn().equals("form")
&& arg.getType() != null
&& !arg.getType().isEmpty()) {
defaultParamType = arg.getType().get(0);
paramType = generateTypeName(arg, arg.getRequired(), arg.getType().get(0));
}

Type<?> paramType = generateTypeName(arg, arg.getRequired(), defaultParamType);
if (paramType == null) {
String defaultParamType = arg.getIn().equals("body")
? InputStream.class.getName()
: Object.class.getName();
paramType = generateTypeName(arg, arg.getRequired(), defaultParamType);
}

if (arg.getTypeSignature() != null) {
// TODO try to find a re-usable data type that matches the type signature
Expand Down Expand Up @@ -717,6 +728,128 @@ protected String generateJavaInterface(CodegenInfo info, CodegenJavaInterface in
return generateJavaInterface(info, interfaceInfo, "jakarta");
}

private Map<String, String> computeRequestBodyTypes(Document document) throws IOException {
JsonNode json = mapper.readTree(Library.writeDocumentToJSONString(document));
Map<String, String> arrayMapTypes = computeArrayMapTypes(json);
Map<String, String> requestBodyTypes = new HashMap<>();

JsonNode paths = json.path("paths");
if (!paths.isObject()) {
return requestBodyTypes;
}

paths.fields().forEachRemaining(pathEntry -> {
JsonNode pathItem = pathEntry.getValue();
pathItem.fields().forEachRemaining(methodEntry -> {
if (!HTTP_METHODS.contains(methodEntry.getKey())) {
return;
}

JsonNode operation = methodEntry.getValue();
String operationId = textValue(operation, "operationId");
if (operationId == null) {
return;
}

JsonNode requestBody = operation.path("requestBody");
String requestType = resolveBodyType(requestBody, arrayMapTypes);
if (requestType != null) {
requestBodyTypes.put(operationId, requestType);
}
});
});

return requestBodyTypes;
}

private Map<String, String> computeArrayMapTypes(JsonNode json) {
Map<String, String> arrayMapTypes = new HashMap<>();
JsonNode schemas = json.path("components").path("schemas");
if (!schemas.isObject()) {
return arrayMapTypes;
}

schemas.fields().forEachRemaining(entry -> {
JsonNode schema = entry.getValue();
JsonNode typeNode = schema.path("x-codegen-type");
if (!typeNode.isTextual() || !"ArrayMap".equals(typeNode.asText())) {
return;
}

JsonNode additionalProperties = schema.path("additionalProperties");
if (!additionalProperties.isObject()) {
return;
}

String valueType = resolveSchemaType(additionalProperties, arrayMapTypes);
arrayMapTypes.put(entry.getKey(), "java.util.Map<String, " + valueType + ">");
});

return arrayMapTypes;
}

private String resolveBodyType(JsonNode requestBody, Map<String, String> arrayMapTypes) {
JsonNode content = requestBody.path("content");
if (!content.isObject() || content.size() == 0) {
return null;
}

JsonNode mediaType = content.elements().next();
JsonNode schema = mediaType.path("schema");
if (!schema.isObject()) {
return null;
}

return resolveSchemaType(schema, arrayMapTypes);
}

private String resolveSchemaType(JsonNode schema, Map<String, String> arrayMapTypes) {
JsonNode ref = schema.get("$ref");
if (ref != null && ref.isTextual()) {
String refName = ref.asText();
String refKey = refName.substring(refName.lastIndexOf('/') + 1);
String arrayMapType = arrayMapTypes.get(refKey);
if (arrayMapType != null) {
return arrayMapType;
}
return CodegenUtil.schemaRefToFQCN(settings, document, refName, this.settings.javaPackage + ".beans");
}

JsonNode type = schema.get("type");
if (type != null && type.isTextual()) {
switch (type.asText()) {
case "array":
JsonNode items = schema.get("items");
String itemType = items != null ? resolveSchemaType(items, arrayMapTypes) : "java.lang.Object";
return "java.util.List<" + itemType + ">";
case "string":
return "java.lang.String";
case "integer":
return "java.lang.Integer";
case "number":
return "java.lang.Double";
case "boolean":
return "java.lang.Boolean";
case "object":
JsonNode additionalProperties = schema.get("additionalProperties");
if (additionalProperties != null && additionalProperties.isObject()) {
String valueType = resolveSchemaType(additionalProperties, arrayMapTypes);
return "java.util.Map<String, " + valueType + ">";
}
return "java.lang.Object";
default:
return "java.lang.Object";
}
}

return "java.lang.Object";
}

private String textValue(JsonNode node, String field) {
JsonNode value = node.get(field);
return value != null && value.isTextual() ? value.asText() : null;
}

public static Properties getFormatterProperties() {
Properties formattingProperties = new Properties();
formattingProperties.setProperty("org.eclipse.jdt.core.formatter.indentation.size", "2");
Expand Down Expand Up @@ -760,6 +893,14 @@ protected Type<?> generateTypeName(CodegenJavaSchema schema, Boolean required, S
required = Boolean.FALSE;
}

String existingJavaType = schema.getExistingJavaType();
if (existingJavaType != null && !existingJavaType.isBlank()) {
if ("APICURIO_CODEGEN_BYTE_ARRAY_REPRESENTATION".equals(existingJavaType)) {
return parseType("byte[]");
}
return parseType(existingJavaType);
}

String collection = schema.getCollection();
List<String> type = Optional.ofNullable(schema.getType()).orElseGet(Collections::emptyList);
String format = schema.getFormat();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class CodegenJavaSchema {
private Long maxProperties;
private Long minProperties;
private String defaultValue;
private String existingJavaType;

public void setType(String type) {
this.type = Collections.singletonList(type);
Expand All @@ -36,8 +37,8 @@ public boolean isNullable() {
@Override
public int hashCode() {
return Objects.hash(collection, constant, defaultValue, exclusiveMaximum, exclusiveMinimum, format, maxItems,
maxLength, maxProperties, maximum, minItems, minLength, minProperties, minimum, pattern, type,
uniqueItems);
maxLength, maxProperties, maximum, minItems, minLength, minProperties, minimum, pattern,
existingJavaType, type, uniqueItems);
}

@Override
Expand All @@ -57,7 +58,8 @@ public boolean equals(Object obj) {
&& Objects.equals(maxProperties, other.maxProperties) && Objects.equals(maximum, other.maximum)
&& Objects.equals(minItems, other.minItems) && Objects.equals(minLength, other.minLength)
&& Objects.equals(minProperties, other.minProperties) && Objects.equals(minimum, other.minimum)
&& Objects.equals(type, other.type) && Objects.equals(uniqueItems, other.uniqueItems);
&& Objects.equals(existingJavaType, other.existingJavaType) && Objects.equals(type, other.type)
&& Objects.equals(uniqueItems, other.uniqueItems);
}

/**
Expand Down Expand Up @@ -299,4 +301,18 @@ public void setDefaultValue(String defaultValue) {
this.defaultValue = defaultValue;
}

/**
* @return the existingJavaType
*/
public String getExistingJavaType() {
return existingJavaType;
}

/**
* @param existingJavaType the existingJavaType to set
*/
public void setExistingJavaType(String existingJavaType) {
this.existingJavaType = existingJavaType;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,15 @@ private void setSchemaProperties(CodegenJavaSchema target, OpenApi3xSchema schem

OpenApi31Schema schema31 = (OpenApi31Schema) schema;

JsonNode existingJavaType = CodegenUtil.getExtension(schema31, "existingJavaType");
if (existingJavaType != null && existingJavaType.isTextual()) {
String javaType = existingJavaType.asText();
target.setExistingJavaType(javaType);
if (javaType.startsWith("java.util.Map<")) {
target.setCollection("map");
}
}

target.setType((List<String>) null);
String $ref = schema31.get$ref();

Expand All @@ -606,15 +615,11 @@ private void setSchemaProperties(CodegenJavaSchema target, OpenApi3xSchema schem
} else if (containsValue(schema31.getType(), "object")) {
setIfPresent(() -> toStringList(schema31.getType()), target::setType);
setIfPresent(schema::getFormat, target::setFormat);
// TODO: Consider representing object as map
//if (schema.getAdditionalProperties() != null && schema.getAdditionalProperties().isSchema()) {
// setSchemaProperties(target, (OpenApi3xSchema) schema.getAdditionalProperties().asSchema());
//}
//
//setIfPresent(schema::isNullable, target::setNullable);
//setIfPresent(schema::getMaxProperties, value -> target.setMaxProperties(value.longValue()));
//setIfPresent(schema::getMinProperties, value -> target.setMinProperties(value.longValue()));
//target.setCollection("map");
String mapJavaType = resolveMapJavaType(schema31);
if (mapJavaType != null) {
target.setExistingJavaType(mapJavaType);
target.setCollection("map");
}
} else if (containsValue(schema31.getType(), "string")) {
setIfPresent(() -> toStringList(schema31.getType()), target::setType);
setIfPresent(schema::getFormat, target::setFormat);
Expand Down Expand Up @@ -652,6 +657,20 @@ private void setSchemaProperties(CodegenJavaSchema target, OpenApi3xSchema schem
});
}

private String resolveMapJavaType(OpenApi31Schema schema) {
if (schema.getAdditionalProperties() == null || !schema.getAdditionalProperties().isSchema()) {
JsonNode existingJavaType = CodegenUtil.getExtension(schema, "existingJavaType");
if (existingJavaType != null && existingJavaType.isTextual()) {
return existingJavaType.asText();
}
return null;
}

String valueType = CodegenUtil.resolveJavaType(settings, (Document) schema.root(),
(OpenApi31Schema) schema.getAdditionalProperties().asSchema(), this.settings.getJavaPackage() + ".beans");
return "java.util.Map<String, " + valueType + ">";
}

private <T> void setIfPresent(Supplier<T> source, Consumer<T> target) {
T value = source.get();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.apitomy.datamodels.TraverserDirection;
import io.apitomy.datamodels.models.Document;
import io.apitomy.datamodels.models.openapi.v3x.v31.visitors.OpenApi31Visitor;
import io.apitomy.hub.api.codegen.JaxRsProjectSettings;

/**
* Used to preprocess an OpenAPI document in a variety of ways with the intent of making the
Expand All @@ -28,27 +29,34 @@
*/
public class DocumentPreProcessor {

private static OpenApi31Visitor [] processors = {
new OpenApiLongSimpleTypeProcessor(),
new OpenApiDateTimeSimpleTypeProcessor(),
new OpenApiByteSimpleTypeProcessor(),
new OpenApiMapDataTypeProcessor(),
new OpenApiAdditionalPropertiesDataTypeProcessor(),
new OpenApiTypeInliner(),
new OpenApiInlinedSchemaRemover(),
new OpenApiParameterInliner(),
new OpenApiInlinedParameterRemover(),
new OpenApiResponseInliner(),
new OpenApiAllOfProcessor(),
new OpenApiBeanClassExtendsProcessor(),
new OpenApiRequestBodyInliner()
};
private final JaxRsProjectSettings settings;
private final Document document;

public DocumentPreProcessor(JaxRsProjectSettings settings, Document document) {
this.settings = settings;
this.document = document;
}

/**
* Process the model.
* @param document
*/
public void process(Document document) {
OpenApi31Visitor[] processors = {
new OpenApiLongSimpleTypeProcessor(),
new OpenApiDateTimeSimpleTypeProcessor(),
new OpenApiByteSimpleTypeProcessor(),
new OpenApiMapDataTypeProcessor(settings, document),
new OpenApiAdditionalPropertiesDataTypeProcessor(),
new OpenApiTypeInliner(),
new OpenApiInlinedSchemaRemover(),
new OpenApiParameterInliner(),
new OpenApiInlinedParameterRemover(),
new OpenApiResponseInliner(),
new OpenApiAllOfProcessor(),
new OpenApiBeanClassExtendsProcessor(),
new OpenApiRequestBodyInliner()
};
for (OpenApi31Visitor proc : processors) {
Library.visitTree(document, proc, TraverserDirection.down);
}
Expand Down
Loading
Loading