diff --git a/CHANGELOG.md b/CHANGELOG.md
index 341f8ec1f6e..3a8cd8207c8 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
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/jackson/Example.java b/crd-generator/test/src/test/java/io/fabric8/crd/generator/jackson/Example.java
new file mode 100644
index 00000000000..8b81ab78ee8
--- /dev/null
+++ b/crd-generator/test/src/test/java/io/fabric8/crd/generator/jackson/Example.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.fabric8.crd.generator.jackson;
+
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.Version;
+
+@Group("org.example")
+@Version("v1alpha1")
+public class Example extends CustomResource implements Namespaced {
+}
diff --git a/crd-generator/test/src/test/java/io/fabric8/crd/generator/jackson/ExampleSpec.java b/crd-generator/test/src/test/java/io/fabric8/crd/generator/jackson/ExampleSpec.java
new file mode 100644
index 00000000000..efaef1f50bb
--- /dev/null
+++ b/crd-generator/test/src/test/java/io/fabric8/crd/generator/jackson/ExampleSpec.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.fabric8.crd.generator.jackson;
+
+import java.time.LocalDate;
+import java.time.ZonedDateTime;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class ExampleSpec {
+ public ZonedDateTime timestamp;
+ public LocalDate local;
+ public UUID uuid;
+ public Short shortValue;
+ public AtomicBoolean ab;
+}
diff --git a/crd-generator/test/src/test/java/io/fabric8/crd/generator/jackson/JacksonTypeTest.java b/crd-generator/test/src/test/java/io/fabric8/crd/generator/jackson/JacksonTypeTest.java
new file mode 100644
index 00000000000..b59893a3461
--- /dev/null
+++ b/crd-generator/test/src/test/java/io/fabric8/crd/generator/jackson/JacksonTypeTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.fabric8.crd.generator.jackson;
+
+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.Test;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class JacksonTypeTest {
+
+ @Test
+ void testCrd() {
+ CustomResourceDefinition d = Serialization.unmarshal(getClass().getClassLoader()
+ .getResourceAsStream("META-INF/fabric8/examples.org.example-v1.yml"),
+ CustomResourceDefinition.class);
+ assertNotNull(d);
+ assertEquals("Example", d.getSpec().getNames().getKind());
+ Map props = d.getSpec().getVersions().get(0).getSchema().getOpenAPIV3Schema().getProperties()
+ .get("spec").getProperties();
+ assertEquals(5, props.size());
+ assertEquals("number", props.get("timestamp").getType());
+ assertEquals("array", props.get("local").getType());
+ assertEquals("string", props.get("uuid").getType());
+ assertEquals("integer", props.get("shortValue").getType());
+ assertEquals("boolean", props.get("ab").getType());
+ }
+}