diff --git a/CHANGELOG.md b/CHANGELOG.md
index 61cb0ae1b83..e82ddb08f59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
### 6.13-SNAPSHOT
#### Bugs
+* Fix #5866: Addressed cycle in crd generation with Java 19+ and ZonedDateTime
#### Improvements
* Fix #5878: (java-generator) Add implements Editable for extraAnnotations
diff --git a/crd-generator/api/pom.xml b/crd-generator/api/pom.xml
index 0093914d332..49574461d2f 100644
--- a/crd-generator/api/pom.xml
+++ b/crd-generator/api/pom.xml
@@ -35,6 +35,11 @@
kubernetes-client-api
compile
+
+
+ com.fasterxml.jackson.module
+ jackson-module-jsonSchema
+
io.fabric8
diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java
index 68e266807db..c4feaf5b0a1 100644
--- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java
+++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java
@@ -16,7 +16,11 @@
package io.fabric8.crd.generator;
import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
+import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator;
+import com.fasterxml.jackson.module.jsonSchema.types.ArraySchema.Items;
import io.fabric8.crd.generator.InternalSchemaSwaps.SwapResult;
import io.fabric8.crd.generator.annotation.SchemaSwap;
import io.fabric8.crd.generator.utils.Types;
@@ -24,6 +28,7 @@
import io.fabric8.kubernetes.api.model.Duration;
import io.fabric8.kubernetes.api.model.IntOrString;
import io.fabric8.kubernetes.api.model.Quantity;
+import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
import io.sundr.builder.internal.functions.TypeAs;
import io.sundr.model.AnnotationRef;
import io.sundr.model.ClassRef;
@@ -43,6 +48,7 @@
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
@@ -122,6 +128,9 @@ public abstract class AbstractJsonSchema {
public static final String JSON_NODE_TYPE = "com.fasterxml.jackson.databind.JsonNode";
public static final String ANY_TYPE = "io.fabric8.kubernetes.api.model.AnyType";
+ private static final JsonSchemaGenerator GENERATOR;
+ private static final Set COMPLEX_JAVA_TYPES = new HashSet<>();
+
static {
COMMON_MAPPINGS.put(STRING_REF, STRING_MARKER);
COMMON_MAPPINGS.put(DATE_REF, STRING_MARKER);
@@ -138,6 +147,10 @@ public abstract class AbstractJsonSchema {
COMMON_MAPPINGS.put(QUANTITY_REF, INT_OR_STRING_MARKER);
COMMON_MAPPINGS.put(INT_OR_STRING_REF, INT_OR_STRING_MARKER);
COMMON_MAPPINGS.put(DURATION_REF, STRING_MARKER);
+ ObjectMapper mapper = new ObjectMapper();
+ // initialize with client defaults
+ new KubernetesSerialization(mapper, false);
+ GENERATOR = new JsonSchemaGenerator(mapper);
}
public static String getSchemaTypeFor(TypeRef typeRef) {
@@ -853,18 +866,67 @@ private T internalFromImpl(String name, TypeRef typeRef, LinkedHashMap visited,
InternalSchemaSwaps schemaSwaps) {
- if (visited.put(def.getFullyQualifiedName(), name) != null) {
+ String fullyQualifiedName = def.getFullyQualifiedName();
+ T res = resolveJavaClass(fullyQualifiedName);
+ if (res != null) {
+ return res;
+ }
+ if (visited.put(fullyQualifiedName, name) != null) {
throw new IllegalArgumentException(
- "Found a cyclic reference involving the field of type " + def.getFullyQualifiedName() + " starting a field "
+ "Found a cyclic reference involving the field of type " + fullyQualifiedName + " starting a field "
+ visited.entrySet().stream().map(e -> e.getValue() + " >>\n" + e.getKey()).collect(Collectors.joining(".")) + "."
+ name);
}
- T res = internalFromImpl(def, visited, schemaSwaps);
- visited.remove(def.getFullyQualifiedName());
+ res = internalFromImpl(def, visited, schemaSwaps);
+ visited.remove(fullyQualifiedName);
return res;
}
+ private T resolveJavaClass(String fullyQualifiedName) {
+ if ((!fullyQualifiedName.startsWith("java.") && !fullyQualifiedName.startsWith("javax."))
+ || COMPLEX_JAVA_TYPES.contains(fullyQualifiedName)) {
+ return null;
+ }
+ String mapping = null;
+ boolean array = false;
+ try {
+ Class> clazz = Class.forName(fullyQualifiedName);
+ JsonSchema schema = GENERATOR.generateSchema(clazz);
+ if (schema.isArraySchema()) {
+ Items items = schema.asArraySchema().getItems();
+ if (items.isSingleItems()) {
+ array = true;
+ schema = items.asSingleItems().getSchema();
+ }
+ }
+ if (schema.isIntegerSchema()) {
+ mapping = INTEGER_MARKER;
+ } else if (schema.isNumberSchema()) {
+ mapping = NUMBER_MARKER;
+ } else if (schema.isBooleanSchema()) {
+ mapping = BOOLEAN_MARKER;
+ } else if (schema.isStringSchema()) {
+ mapping = STRING_MARKER;
+ }
+ } catch (Exception e) {
+ LOGGER.debug(
+ "Something went wrong with detecting java type schema for {}, will use full introspection instead",
+ fullyQualifiedName, e);
+ }
+ // cache the result for subsequent calls
+ if (mapping != null) {
+ if (array) {
+ return arrayLikeProperty(singleProperty(mapping));
+ }
+ COMMON_MAPPINGS.put(TypeDef.forName(fullyQualifiedName).toReference(), mapping);
+ return singleProperty(mapping);
+ }
+
+ COMPLEX_JAVA_TYPES.add(fullyQualifiedName);
+ return null;
+ }
+
/**
* Builds the schema for specifically handled property types (e.g. intOrString properties)
*
diff --git a/crd-generator/test/src/test/java/io/fabric8/crd/generator/types/TypeMappingsTest.java b/crd-generator/test/src/test/java/io/fabric8/crd/generator/types/TypeMappingsTest.java
index 1540a775613..5eda53475fe 100644
--- a/crd-generator/test/src/test/java/io/fabric8/crd/generator/types/TypeMappingsTest.java
+++ b/crd-generator/test/src/test/java/io/fabric8/crd/generator/types/TypeMappingsTest.java
@@ -16,12 +16,14 @@
package io.fabric8.crd.generator.types;
import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition;
+import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps;
import io.fabric8.kubernetes.client.utils.Serialization;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
+import java.util.Map;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
@@ -40,27 +42,29 @@ void setUp() {
@ParameterizedTest(name = "{0} type maps to {1}")
@MethodSource("targetTypeCases")
void targetType(String propertyName, String expectedType) {
- assertThat(crd.getSpec().getVersions().iterator().next().getSchema().getOpenAPIV3Schema().getProperties())
- .extracting(schema -> schema.get("spec").getProperties())
+ Map properties = crd.getSpec().getVersions().iterator().next().getSchema().getOpenAPIV3Schema()
+ .getProperties().get("spec").getProperties();
+ assertThat(properties)
+ .withFailMessage("Expected %s to be %s, but was %s", propertyName, expectedType, properties.get(propertyName).getType())
.returns(expectedType, specProps -> specProps.get(propertyName).getType());
}
private static Stream targetTypeCases() {
return Stream.of(
Arguments.of("date", "string"),
- Arguments.of("localDate", "object"), // to review
- Arguments.of("localDateTime", "object"), // to review
- Arguments.of("zonedDateTime", "object"), // to review
- Arguments.of("offsetDateTime", "object"), // to review
- Arguments.of("offsetTime", "object"), // to review
- Arguments.of("yearMonth", "object"), // to review
- Arguments.of("monthDay", "object"), // to review
- Arguments.of("instant", "object"), // to review
- Arguments.of("duration", "object"), // to review
- Arguments.of("period", "object"), // to review
- Arguments.of("timestamp", "object"), // to review
+ Arguments.of("localDate", "array"), // to review
+ Arguments.of("localDateTime", "array"), // to review
+ Arguments.of("zonedDateTime", "number"), // to review
+ Arguments.of("offsetDateTime", "number"), // to review
+ Arguments.of("offsetTime", "array"), // to review
+ Arguments.of("yearMonth", "array"), // to review
+ Arguments.of("monthDay", "array"), // to review
+ Arguments.of("instant", "number"), // to review
+ Arguments.of("duration", "integer"), // to review
+ Arguments.of("period", "string"),
+ Arguments.of("timestamp", "integer"), // to review
// Arguments.of("aShort", "integer"), // TODO: Not even present in the CRD
- Arguments.of("aShortObj", "object"), // to review
+ Arguments.of("aShortObj", "integer"),
Arguments.of("aInt", "integer"),
Arguments.of("aIntegerObj", "integer"),
Arguments.of("aLong", "integer"),
@@ -69,21 +73,21 @@ private static Stream targetTypeCases() {
Arguments.of("aDoubleObj", "number"),
Arguments.of("aFloat", "number"),
Arguments.of("aFloatObj", "number"),
- Arguments.of("aNumber", "object"), // to review
- Arguments.of("aBigInteger", "object"), // to review
- Arguments.of("aBigDecimal", "object"), // to review
+ Arguments.of("aNumber", "number"),
+ Arguments.of("aBigInteger", "integer"),
+ Arguments.of("aBigDecimal", "number"),
Arguments.of("aBoolean", "boolean"),
Arguments.of("aBooleanObj", "boolean"),
// Arguments.of("aChar", "string"), // TODO: Not even present in the CRD
- Arguments.of("aCharacterObj", "object"), // to review
+ Arguments.of("aCharacterObj", "string"),
Arguments.of("aCharArray", "array"),
- Arguments.of("aCharSequence", "object"), // to review
+ Arguments.of("aCharSequence", "string"),
Arguments.of("aString", "string"),
Arguments.of("aStringArray", "array"),
// Arguments.of("aByte", "?"), // TODO: Not even present in the CRD
- Arguments.of("aByteObj", "object"), // to review
+ Arguments.of("aByteObj", "integer"),
Arguments.of("aByteArray", "array"), // to review, should be string (base64)
- Arguments.of("uuid", "object")); // to review, should be string
+ Arguments.of("uuid", "string"));
}
}