diff --git a/CHANGELOG.md b/CHANGELOG.md index ae7b6d290ff..6a6947271f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Fix #6008: removing the optional dependency on bouncy castle * Fix #6230: introduced Quantity.multiply(int) to allow for Quantity multiplication by an integer * Fix #6281: use GitHub binary repo for Kube API Tests +* Fix #6282: Allow annotated types with Pattern, Min, and Max with Lists and Maps and CRD generation #### Dependency Upgrade * Fix #6052: Removed dependency on no longer maintained com.github.mifmif:generex diff --git a/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/AbstractJsonSchema.java b/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/AbstractJsonSchema.java index 241da93eaee..0721487a234 100644 --- a/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/AbstractJsonSchema.java +++ b/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/AbstractJsonSchema.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.module.jsonSchema.JsonSchema; import com.fasterxml.jackson.module.jsonSchema.types.ArraySchema.Items; @@ -56,6 +55,9 @@ import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; @@ -68,6 +70,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.TreeMap; @@ -143,7 +146,7 @@ private T resolveRoot(Class definition) { return resolveObject(new LinkedHashMap<>(), schemaSwaps, schema, "kind", "apiVersion", "metadata"); } return resolveProperty(new LinkedHashMap<>(), schemaSwaps, null, - resolvingContext.objectMapper.getSerializationConfig().constructType(definition), schema); + resolvingContext.objectMapper.getSerializationConfig().constructType(definition), schema, null); } /** @@ -156,14 +159,12 @@ private static void consumeRepeatingAnnotation(Class b beanClass = beanClass.getSuperclass(); } } - - void collectValidationRules(BeanProperty beanProperty, List validationRules) { - // TODO: the old logic allowed for picking up the annotation from both the getter and the field - // this requires a messy hack by convention because there doesn't seem to be a way to all annotations - // nor does jackson provide the field - if (beanProperty.getMember() instanceof AnnotatedMethod) { + + Optional getFieldForMethod(BeanProperty beanProperty) { + AnnotatedElement annotated = beanProperty.getMember().getAnnotated(); + if (annotated instanceof Method) { // field first - Method m = ((AnnotatedMethod) beanProperty.getMember()).getMember(); + Method m = (Method)annotated; String name = m.getName(); if (name.startsWith("get") || name.startsWith("set")) { name = name.substring(3); @@ -173,16 +174,31 @@ void collectValidationRules(BeanProperty beanProperty, List validationRules) if (name.length() > 0) { name = Character.toLowerCase(name.charAt(0)) + name.substring(1); } + + try { + return Optional.of(m.getDeclaringClass().getDeclaredField(name)); + } catch (NoSuchFieldException | SecurityException e) { + } + } + return Optional.empty(); + } + + void collectValidationRules(BeanProperty beanProperty, List validationRules) { + // TODO: the old logic allowed for picking up the annotation from both the getter and the field + // this requires a messy hack by convention because there doesn't seem to be a way to all annotations + // nor does jackson provide the field + AnnotatedElement member = beanProperty.getMember().getAnnotated(); + if (member instanceof Method) { + Optional field = getFieldForMethod(beanProperty); try { - Field f = beanProperty.getMember().getDeclaringClass().getDeclaredField(name); - ofNullable(f.getAnnotation(ValidationRule.class)).map(this::from) + field.map(f -> f.getAnnotation(ValidationRule.class)).map(this::from) .ifPresent(validationRules::add); - ofNullable(f.getAnnotation(ValidationRules.class)) + field.map(f -> f.getAnnotation(ValidationRules.class)) .ifPresent(ann -> Stream.of(ann.value()).map(this::from).forEach(validationRules::add)); - } catch (NoSuchFieldException | SecurityException e) { + } catch (SecurityException e) { } // then method - Stream.of(m.getAnnotationsByType(ValidationRule.class)).map(this::from).forEach(validationRules::add); + Stream.of(((Method)member).getAnnotationsByType(ValidationRule.class)).map(this::from).forEach(validationRules::add); return; } @@ -225,8 +241,8 @@ public PropertyMetadata(JsonSchema value, BeanProperty beanProperty) { StringSchema stringSchema = value.asStringSchema(); // only set if ValidationSchemaFactoryWrapper is used this.pattern = stringSchema.getPattern(); - this.max = ofNullable(stringSchema.getMaxLength()).map(Integer::doubleValue).orElse(null); - this.min = ofNullable(stringSchema.getMinLength()).map(Integer::doubleValue).orElse(null); + //this.maxLength = ofNullable(stringSchema.getMaxLength()).map(Integer::doubleValue).orElse(null); + //this.minLength = ofNullable(stringSchema.getMinLength()).map(Integer::doubleValue).orElse(null); } else { // TODO: process the other schema types for validation values } @@ -333,7 +349,7 @@ private T resolveObject(LinkedHashMap visited, InternalSchemaSwa type = resolvingContext.objectMapper.getSerializationConfig().constructType(propertyMetadata.schemaFrom); } - T schema = resolveProperty(visited, schemaSwaps, name, type, propertySchema); + T schema = resolveProperty(visited, schemaSwaps, name, type, propertySchema, beanProperty); propertyMetadata.updateSchema(schema); @@ -378,15 +394,19 @@ static String toFQN(LinkedHashMap visited, String name) { } private T resolveProperty(LinkedHashMap visited, InternalSchemaSwaps schemaSwaps, String name, - JavaType type, JsonSchema jacksonSchema) { + JavaType type, JsonSchema jacksonSchema, BeanProperty beanProperty) { if (jacksonSchema.isArraySchema()) { Items items = jacksonSchema.asArraySchema().getItems(); + if (items == null) { // raw collection + throw new IllegalStateException(String.format("Untyped collection %s", name)); + } if (items.isArrayItems()) { throw new IllegalStateException("not yet supported"); } JsonSchema arraySchema = jacksonSchema.asArraySchema().getItems().asSingleItems().getSchema(); - final T schema = resolveProperty(visited, schemaSwaps, name, type.getContentType(), arraySchema); + final T schema = resolveProperty(visited, schemaSwaps, name, type.getContentType(), arraySchema, null); + handleTypeAnnotations(schema, beanProperty, List.class, 0); return arrayLikeProperty(schema); } else if (jacksonSchema.isIntegerSchema()) { return singleProperty("integer"); @@ -440,7 +460,8 @@ private T resolveProperty(LinkedHashMap visited, InternalSchemaS final JavaType valueType = type.getContentType(); JsonSchema mapValueSchema = ((SchemaAdditionalProperties) ((ObjectSchema) jacksonSchema).getAdditionalProperties()) .getJsonSchema(); - T component = resolveProperty(visited, schemaSwaps, name, valueType, mapValueSchema); + T component = resolveProperty(visited, schemaSwaps, name, valueType, mapValueSchema, null); + handleTypeAnnotations(component, beanProperty, Map.class, 1); return mapLikeProperty(component); } @@ -464,8 +485,34 @@ private T resolveProperty(LinkedHashMap visited, InternalSchemaS return res; } + private void handleTypeAnnotations(final T schema, BeanProperty beanProperty, Class containerType, int typeIndex) { + if (beanProperty == null || !containerType.equals(beanProperty.getType().getRawClass())) { + return; + } + + AnnotatedElement member = beanProperty.getMember().getAnnotated(); + AnnotatedType fieldType = null; + AnnotatedType type = null; + if (member instanceof Field) { + fieldType = ((Field) member).getAnnotatedType(); + } else if (member instanceof Method) { + fieldType = getFieldForMethod(beanProperty).map(Field::getAnnotatedType).orElse(null); + type = ((Method) member).getAnnotatedReceiverType(); + } + + Stream.of(fieldType, type).filter(o -> !Objects.isNull(o)) + .filter(AnnotatedParameterizedType.class::isInstance).map(AnnotatedParameterizedType.class::cast) + .map(AnnotatedParameterizedType::getAnnotatedActualTypeArguments).map(a -> { + return a[typeIndex]; + }).forEach(at -> { + Optional.ofNullable(at.getAnnotation(Pattern.class)).ifPresent(a -> schema.setPattern(a.value())); + Optional.ofNullable(at.getAnnotation(Min.class)).ifPresent(a -> schema.setMinimum(a.value())); + Optional.ofNullable(at.getAnnotation(Max.class)).ifPresent(a -> schema.setMaximum(a.value())); + }); + } + /** - * we've added support for ignoring an enum values, which complicates this processing + * we've added support for ignoring enum values, which complicates this processing * as that is something not supported directly by jackson */ private Set findIgnoredEnumConstants(JavaType type) { diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/annotated/AnnotatedSpec.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/annotated/AnnotatedSpec.java index 55cfe8837ab..43c4297ff07 100644 --- a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/annotated/AnnotatedSpec.java +++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/annotated/AnnotatedSpec.java @@ -29,6 +29,8 @@ import lombok.Data; import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; @Data public class AnnotatedSpec { @@ -57,6 +59,9 @@ public class AnnotatedSpec { private String numInt; private String numFloat; private ZonedDateTime issuedAt; + + private List<@Pattern("[a-z].*") String> typeAnnotationCollection; + private Map typeAnnotationMap; @JsonIgnore private int ignoredFoo; diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/person/Person.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/person/Person.java index 72c57dd688f..ba8d450ffaa 100644 --- a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/person/Person.java +++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/person/Person.java @@ -15,6 +15,8 @@ */ package io.fabric8.crdv2.example.person; +import io.fabric8.generator.annotation.Pattern; + import java.util.List; import java.util.Optional; @@ -24,7 +26,7 @@ public class Person { public Optional middleName; public String lastName; public int birthYear; - public List hobbies; + public List<@Pattern(".*ball") String> hobbies; public AddressList addresses; public Type type; diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/v1/JsonSchemaTest.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/v1/JsonSchemaTest.java index bd55e951a86..d702d4a294b 100644 --- a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/v1/JsonSchemaTest.java +++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/v1/JsonSchemaTest.java @@ -95,7 +95,9 @@ void shouldCreateJsonSchemaFromClass() { assertEquals(2, addressTypes.size()); assertTrue(addressTypes.contains("home")); assertTrue(addressTypes.contains("work")); - + assertEquals(".*ball", properties.get("hobbies").getItems() + .getSchema().getPattern()); + schema = JsonSchema.from(Basic.class); assertNotNull(schema); properties = schema.getProperties(); @@ -116,7 +118,7 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti assertNotNull(schema); Map properties = assertSchemaHasNumberOfProperties(schema, 2); final JSONSchemaProps specSchema = properties.get("spec"); - Map spec = assertSchemaHasNumberOfProperties(specSchema, 20); + Map spec = assertSchemaHasNumberOfProperties(specSchema, 22); // check descriptions are present assertTrue(spec.containsKey("from-field")); @@ -154,6 +156,12 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti assertTrue(required.contains("emptySetter")); assertTrue(required.contains("emptySetter2")); assertTrue(required.contains("from-getter")); + + assertEquals("[a-z].*", spec.get("typeAnnotationCollection").getItems() + .getSchema().getPattern()); + JSONSchemaProps mapSchema = spec.get("typeAnnotationMap").getAdditionalProperties().getSchema(); + assertEquals(255, mapSchema.getMaximum()); + assertEquals(1.0, mapSchema.getMinimum()); // check ignored fields assertFalse(spec.containsKey("ignoredFoo")); diff --git a/generator-annotations/src/main/java/io/fabric8/generator/annotation/Max.java b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Max.java index 3cbdb5b3ecd..7489307428b 100644 --- a/generator-annotations/src/main/java/io/fabric8/generator/annotation/Max.java +++ b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Max.java @@ -15,7 +15,10 @@ */ package io.fabric8.generator.annotation; -import java.lang.annotation.*; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; /** * Java representation of the {@code maximum} field of JSONSchemaProps. @@ -25,7 +28,7 @@ * Kubernetes Docs - API Reference - CRD v1 - JSONSchemaProps * */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE_USE }) @Retention(RetentionPolicy.RUNTIME) public @interface Max { double value(); diff --git a/generator-annotations/src/main/java/io/fabric8/generator/annotation/Min.java b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Min.java index 4b40b666eac..2a472ceedb4 100644 --- a/generator-annotations/src/main/java/io/fabric8/generator/annotation/Min.java +++ b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Min.java @@ -15,7 +15,10 @@ */ package io.fabric8.generator.annotation; -import java.lang.annotation.*; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; /** * Java representation of the {@code minimum} field of JSONSchemaProps. @@ -25,7 +28,7 @@ * Kubernetes Docs - API Reference - CRD v1 - JSONSchemaProps * */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE_USE }) @Retention(RetentionPolicy.RUNTIME) public @interface Min { double value(); diff --git a/generator-annotations/src/main/java/io/fabric8/generator/annotation/Pattern.java b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Pattern.java index def7c5fdaa9..5335131a197 100644 --- a/generator-annotations/src/main/java/io/fabric8/generator/annotation/Pattern.java +++ b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Pattern.java @@ -15,7 +15,10 @@ */ package io.fabric8.generator.annotation; -import java.lang.annotation.*; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; /** * Java representation of the {@code pattern} field of JSONSchemaProps. @@ -25,7 +28,7 @@ * Kubernetes Docs - API Reference - CRD v1 - JSONSchemaProps * */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE_USE }) @Retention(RetentionPolicy.RUNTIME) public @interface Pattern { String value();