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
153 changes: 149 additions & 4 deletions core/src/main/java/io/apitomy/hub/api/codegen/OpenApi2JaxRs.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,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 +382,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 @@ -535,6 +537,10 @@ void sortImports(Importer<?> javaSource) {
* @param interfaceInfo
*/
protected String generateJavaInterface(CodegenInfo info, CodegenJavaInterface interfaceInfo, String topLevelPackage) {
return generateJavaInterfaceSource(info, interfaceInfo, topLevelPackage);
}

protected String generateJavaInterfaceSource(CodegenInfo info, CodegenJavaInterface interfaceInfo, String topLevelPackage) {
String jaxRsPath = info.getContextRoot() + interfaceInfo.getPath();
final Parser markdownParser = Parser.builder().build();
final HtmlRenderer htmlRenderer = HtmlRenderer.builder().build();
Expand Down Expand Up @@ -639,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 @@ -714,6 +728,129 @@ 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 (!Set.of("get", "put", "post", "delete", "options", "head", "patch", "trace")
.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 @@ -757,6 +894,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 @@ -583,7 +583,14 @@ private void setSchemaProperties(CodegenJavaSchema target, OpenApi3xSchema schem
return;
}

OpenApi31Schema schema31 = (OpenApi31Schema) schema;
JsonNode existingJavaType = CodegenUtil.getExtension(schema, "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,17 +613,13 @@ 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");
} else if (containsValue(schema31.getType(), "string")) {
setIfPresent(() -> toStringList(schema31.getType()), target::setType);
String mapJavaType = resolveMapJavaType(schema);
if (mapJavaType != null) {
target.setExistingJavaType(mapJavaType);
target.setCollection("map");
}
} else if (containsValue(schema.getType(), "string")) {
setIfPresent(() -> toStringList(schema.getType()), target::setType);
setIfPresent(schema::getFormat, target::setFormat);
setIfPresent(schema::getMaxLength, value -> target.setMaxLength(value.longValue()));
setIfPresent(schema::getMinLength, value -> target.setMinLength(value.longValue()));
Expand Down Expand Up @@ -652,6 +655,60 @@ 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 = resolveJavaType((OpenApi31Schema) schema.getAdditionalProperties().asSchema());
return "java.util.Map<String, " + valueType + ">";
}

private String resolveJavaType(OpenApi31Schema schema) {
if (schema == null) {
return "java.lang.Object";
}

if (schema.get$ref() != null && !schema.get$ref().isBlank()) {
return CodegenUtil.schemaRefToFQCN(settings, (Document) schema.root(), schema.get$ref(),
this.settings.getJavaPackage() + ".beans");
}

if (containsValue(schema.getType(), "array")) {
String itemType = "java.lang.Object";
if (schema.getItems() != null) {
itemType = resolveJavaType((OpenApi31Schema) schema.getItems());
}
return "java.util.List<" + itemType + ">";
}

if (containsValue(schema.getType(), "string")) {
return "java.lang.String";
}
if (containsValue(schema.getType(), "integer")) {
return "java.lang.Integer";
}
if (containsValue(schema.getType(), "number")) {
return "java.lang.Double";
}
if (containsValue(schema.getType(), "boolean")) {
return "java.lang.Boolean";
}
if (containsValue(schema.getType(), "object")) {
if (schema.getAdditionalProperties() != null && schema.getAdditionalProperties().isSchema()) {
String valueType = resolveJavaType((OpenApi31Schema) schema.getAdditionalProperties().asSchema());
return "java.util.Map<String, " + valueType + ">";
}
return "java.lang.Object";
}

return "java.lang.Object";
}

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

Expand Down
Loading