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()); + } +}