diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/FilterChainSchemaFactoryWrapper.java b/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/FilterChainSchemaFactoryWrapper.java new file mode 100644 index 00000000..b67eb910 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/FilterChainSchemaFactoryWrapper.java @@ -0,0 +1,87 @@ +package com.fasterxml.jackson.module.jsonSchema.customProperties; + +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.module.jsonSchema.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.customProperties.filter.BeanPropertyFilter; +import com.fasterxml.jackson.module.jsonSchema.customProperties.transformer.JsonSchemaTransformer; +import com.fasterxml.jackson.module.jsonSchema.factories.ObjectVisitor; +import com.fasterxml.jackson.module.jsonSchema.factories.ObjectVisitorDecorator; +import com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper; +import com.fasterxml.jackson.module.jsonSchema.types.ObjectSchema; + +import java.util.Iterator; +import java.util.List; + +/** + * This subtype of {@link com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper} allows + * you to filter out {@link com.fasterxml.jackson.databind.BeanProperty} from generating schema by applying + * to each property {@link com.fasterxml.jackson.module.jsonSchema.customProperties.filter.BeanPropertyFilter}. + * + * BeanProperty will be excluded if at least one filter excludes it. + * + * This wrapper also uses {@link com.fasterxml.jackson.module.jsonSchema.customProperties.transformer.JsonSchemaTransformer} + * transformers to apply some additional transformation of {@link com.fasterxml.jackson.module.jsonSchema.JsonSchema} + * + * @author wololock + */ +public class FilterChainSchemaFactoryWrapper extends SchemaFactoryWrapper { + + private final List filters; + + private final List transformers; + + public FilterChainSchemaFactoryWrapper(FilterChainSchemaFactoryWrapperFactory wrapperFactory) { + super(wrapperFactory); + this.filters = wrapperFactory.getFilters(); + this.transformers = wrapperFactory.getTransformers(); + } + + @Override + public JsonObjectFormatVisitor expectObjectFormat(JavaType convertedType) { + return new ObjectVisitorDecorator((ObjectVisitor) super.expectObjectFormat(convertedType)) { + @Override + public void optionalProperty(BeanProperty writer) throws JsonMappingException { + boolean allowed = applyFilters(writer); + if (allowed) { + super.optionalProperty(writer); + applyTransformations(writer); + } + } + + @Override + public void property(BeanProperty writer) throws JsonMappingException { + boolean allowed = applyFilters(writer); + if (allowed) { + super.property(writer); + applyTransformations(writer); + } + } + + private boolean applyFilters(BeanProperty writer) { + boolean allowed = true; + Iterator iterator = filters.iterator(); + while (iterator.hasNext() && allowed) { + allowed = iterator.next().test(writer); + } + return allowed; + } + + private void applyTransformations(BeanProperty beanProperty) { + if (!transformers.isEmpty()) { + JsonSchema jsonSchema = getPropertySchema(beanProperty); + for (JsonSchemaTransformer transformer : transformers) { + jsonSchema = transformer.transform(jsonSchema, beanProperty); + } + } + } + + private JsonSchema getPropertySchema(BeanProperty beanProperty) { + return ((ObjectSchema) getSchema()).getProperties().get(beanProperty.getName()); + } + }; + } + +} diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/FilterChainSchemaFactoryWrapperFactory.java b/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/FilterChainSchemaFactoryWrapperFactory.java new file mode 100644 index 00000000..809c9411 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/FilterChainSchemaFactoryWrapperFactory.java @@ -0,0 +1,67 @@ +package com.fasterxml.jackson.module.jsonSchema.customProperties; + +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.module.jsonSchema.customProperties.filter.BeanPropertyFilter; +import com.fasterxml.jackson.module.jsonSchema.customProperties.transformer.JsonSchemaTransformer; +import com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper; +import com.fasterxml.jackson.module.jsonSchema.factories.VisitorContext; +import com.fasterxml.jackson.module.jsonSchema.factories.WrapperFactory; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * Creates {@link FilterChainSchemaFactoryWrapper} with + * injected list of {@link com.fasterxml.jackson.module.jsonSchema.customProperties.filter.BeanPropertyFilter} filters + * and additional list of {@link com.fasterxml.jackson.module.jsonSchema.customProperties.transformer.JsonSchemaTransformer} + * transformers. + * + * This class is thread-safe. + * + * @author wololock + */ +public class FilterChainSchemaFactoryWrapperFactory extends WrapperFactory { + + /** + * Chain of filters + * + * Only properties that match all filters will be included in final + * JSON schema. + */ + private final List filters; + + /** + * Additional transformations that have to be applied to filtered + * bean properties. + */ + private final List transformers; + + public FilterChainSchemaFactoryWrapperFactory(List filters, List transformers) { + this.filters = Collections.unmodifiableList(filters != null ? filters : new LinkedList()); + this.transformers = Collections.unmodifiableList(transformers != null ? transformers : new LinkedList()); + } + + public List getFilters() { + return filters; + } + + public List getTransformers() { + return transformers; + } + + @Override + public SchemaFactoryWrapper getWrapper(SerializerProvider p) { + SchemaFactoryWrapper wrapper = new FilterChainSchemaFactoryWrapper(this); + wrapper.setProvider(p); + return wrapper; + } + + @Override + public SchemaFactoryWrapper getWrapper(SerializerProvider p, VisitorContext rvc) { + SchemaFactoryWrapper wrapper = new FilterChainSchemaFactoryWrapper(this); + wrapper.setProvider(p); + wrapper.setVisitorContext(rvc); + return wrapper; + } +} diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/filter/BeanPropertyFilter.java b/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/filter/BeanPropertyFilter.java new file mode 100644 index 00000000..90e7ea85 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/filter/BeanPropertyFilter.java @@ -0,0 +1,13 @@ +package com.fasterxml.jackson.module.jsonSchema.customProperties.filter; + +import com.fasterxml.jackson.databind.BeanProperty; + +/** + * Checks if given {@link com.fasterxml.jackson.databind.BeanProperty} + * follows filtering rule. + * + * @author wololock + */ +public interface BeanPropertyFilter { + boolean test(BeanProperty property); +} diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/filter/RuntimeAnnotatedBeanPropertyFilter.java b/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/filter/RuntimeAnnotatedBeanPropertyFilter.java new file mode 100644 index 00000000..52ef2b55 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/filter/RuntimeAnnotatedBeanPropertyFilter.java @@ -0,0 +1,44 @@ +package com.fasterxml.jackson.module.jsonSchema.customProperties.filter; + +import com.fasterxml.jackson.databind.BeanProperty; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * Rejects {@link com.fasterxml.jackson.databind.BeanProperty} if it's + * annotated with at least one given annotation. + * + * @author wololock + */ +public class RuntimeAnnotatedBeanPropertyFilter implements BeanPropertyFilter { + + private final List> rejectedAnnotations; + + public RuntimeAnnotatedBeanPropertyFilter(List> rejectedAnnotations) { + this.rejectedAnnotations = Collections.unmodifiableList(rejectedAnnotations); + } + + public RuntimeAnnotatedBeanPropertyFilter(Class ...classes) { + this(Arrays.asList(classes)); + } + + @Override + public boolean test(BeanProperty property) { + boolean accept = true; + if (hasAnnotations(property)) { + Iterator> iterator = rejectedAnnotations.iterator(); + while (accept && iterator.hasNext()) { + accept = !property.getMember().hasAnnotation(iterator.next()); + } + } + return accept; + } + + private boolean hasAnnotations(BeanProperty property) { + return property.getMember().annotations().iterator().hasNext(); + } +} diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/transformer/JsonSchemaTransformer.java b/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/transformer/JsonSchemaTransformer.java new file mode 100644 index 00000000..ce55dccc --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/transformer/JsonSchemaTransformer.java @@ -0,0 +1,14 @@ +package com.fasterxml.jackson.module.jsonSchema.customProperties.transformer; + +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.module.jsonSchema.JsonSchema; + +/** + * JsonSchemaTransformer defines additional {@link com.fasterxml.jackson.module.jsonSchema.JsonSchema} + * transformation. + * + * @author wololock + */ +public interface JsonSchemaTransformer { + JsonSchema transform(JsonSchema jsonSchema, BeanProperty beanProperty); +} diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ObjectVisitorDecorator.java b/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ObjectVisitorDecorator.java new file mode 100644 index 00000000..ae5c794c --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ObjectVisitorDecorator.java @@ -0,0 +1,57 @@ +package com.fasterxml.jackson.module.jsonSchema.factories; + +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.module.jsonSchema.JsonSchema; + +/** + * @author cponomaryov + */ +public class ObjectVisitorDecorator implements JsonObjectFormatVisitor, JsonSchemaProducer { + + protected ObjectVisitor objectVisitor; + + public ObjectVisitorDecorator(ObjectVisitor objectVisitor) { + this.objectVisitor = objectVisitor; + } + + @Override + public JsonSchema getSchema() { + return objectVisitor.getSchema(); + } + + @Override + public SerializerProvider getProvider() { + return objectVisitor.getProvider(); + } + + @Override + public void setProvider(SerializerProvider serializerProvider) { + objectVisitor.setProvider(serializerProvider); + } + + @Override + public void optionalProperty(BeanProperty writer) throws JsonMappingException { + objectVisitor.optionalProperty(writer); + } + + @Override + public void optionalProperty(String name, JsonFormatVisitable handler, JavaType propertyTypeHint) throws JsonMappingException { + objectVisitor.optionalProperty(name, handler, propertyTypeHint); + } + + @Override + public void property(BeanProperty writer) throws JsonMappingException { + objectVisitor.property(writer); + } + + @Override + public void property(String name, JsonFormatVisitable handler, JavaType propertyTypeHint) throws JsonMappingException { + objectVisitor.property(name, handler, propertyTypeHint); + } + +} \ No newline at end of file diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestFilterChainSchemaFactoryWrapper.java b/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestFilterChainSchemaFactoryWrapper.java new file mode 100644 index 00000000..0323424c --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/module/jsonSchema/TestFilterChainSchemaFactoryWrapper.java @@ -0,0 +1,239 @@ +package com.fasterxml.jackson.module.jsonSchema; + + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.jsonSchema.customProperties.FilterChainSchemaFactoryWrapper; +import com.fasterxml.jackson.module.jsonSchema.customProperties.FilterChainSchemaFactoryWrapperFactory; +import com.fasterxml.jackson.module.jsonSchema.customProperties.filter.BeanPropertyFilter; +import com.fasterxml.jackson.module.jsonSchema.customProperties.filter.RuntimeAnnotatedBeanPropertyFilter; +import com.fasterxml.jackson.module.jsonSchema.customProperties.transformer.JsonSchemaTransformer; +import com.fasterxml.jackson.module.jsonSchema.types.ObjectSchema; +import com.fasterxml.jackson.module.jsonSchema.types.StringSchema; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.junit.Assert.*; + +/** + * @author wololock + */ +public class TestFilterChainSchemaFactoryWrapper { + + private static final String EXPECTED_NAME_ID = UUID.randomUUID().toString(); + + @Retention(RetentionPolicy.RUNTIME) + private static @interface FilterThatOne { + } + + private static class InternalValue { + private BigDecimal value; + + private boolean required; + + public BigDecimal getValue() { + return value; + } + + public void setValue(BigDecimal value) { + this.value = value; + } + + @FilterThatOne + public boolean isRequired() { + return required; + } + + public void setRequired(boolean required) { + this.required = required; + } + } + + private static class TestBean { + private String name; + + @FilterThatOne + private String oldName; + + private InternalValue internalValue; + + private String lastName; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getOldName() { + return oldName; + } + + public void setOldName(String oldName) { + this.oldName = oldName; + } + + public InternalValue getInternalValue() { + return internalValue; + } + + public void setInternalValue(InternalValue internalValue) { + this.internalValue = internalValue; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + } + + private FilterChainSchemaFactoryWrapperFactory factory; + + private FilterChainSchemaFactoryWrapper visitor; + + private ObjectMapper objectMapper; + + private JsonSchema jsonSchema; + + @Before + public void setup() throws JsonMappingException { + + List filters = Arrays.asList( + new RuntimeAnnotatedBeanPropertyFilter(FilterThatOne.class, Deprecated.class), + + // This test filter removes properties which names start + // with 'value' + new BeanPropertyFilter() { + @Override + public boolean test(BeanProperty property) { + return !property.getName().startsWith("value"); + } + } + ); + + List transformers = Arrays.asList( + new JsonSchemaTransformer() { + @Override + public JsonSchema transform(JsonSchema jsonSchema, BeanProperty beanProperty) { + if (jsonSchema.isStringSchema() && "name".equals(beanProperty.getName())) { + StringSchema stringSchema = jsonSchema.asStringSchema(); + stringSchema.setId(EXPECTED_NAME_ID); + } + return jsonSchema; + } + } + ); + + factory = new FilterChainSchemaFactoryWrapperFactory(filters, transformers); + visitor = new FilterChainSchemaFactoryWrapper(factory); + objectMapper = new ObjectMapper(); + objectMapper.acceptJsonFormatVisitor(TestBean.class, visitor); + jsonSchema = visitor.finalSchema(); + } + + /** + * Un-ignore this test if you want to see how final json schema looks like. + * @throws JsonProcessingException + */ + @Test + @Ignore + public void shouldPrintlnGeneratedJsonSchema() throws JsonProcessingException { + System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonSchema)); + } + + + @Test + public void shouldFilterOutOldNamePropertyOfTestBeanClass() throws JsonProcessingException { + //given: + ObjectSchema objectSchema = jsonSchema.asObjectSchema(); + //when: + Map properties = objectSchema.getProperties(); + //then: + assertFalse(properties.containsKey("oldName")); + } + + + @Test + public void shouldNotFilterNamePropertyOfTestBeanClass() { + //given: + ObjectSchema objectSchema = jsonSchema.asObjectSchema(); + //when: + Map properties = objectSchema.getProperties(); + //then: + assertTrue(properties.containsKey("name")); + } + + @Test + public void shouldNotFilterInternalValuePropertyOfTestBeanClass() { + //given: + ObjectSchema objectSchema = jsonSchema.asObjectSchema(); + //when: + Map properties = objectSchema.getProperties(); + //then: + assertTrue(properties.containsKey("internalValue")); + } + + @Test + public void shouldFilterOutRequiredPropertyOfInternalValueClass() { + //given: + ObjectSchema objectSchema = jsonSchema.asObjectSchema() + .getProperties() + .get("internalValue") + .asObjectSchema(); + //when: + Map properties = objectSchema.getProperties(); + //then: + assertFalse(properties.containsKey("required")); + } + + @Test + public void shouldFilterOutValuePropertyOfInternalValueClass() { + //given: + ObjectSchema objectSchema = jsonSchema.asObjectSchema() + .getProperties() + .get("internalValue") + .asObjectSchema(); + //when: + Map properties = objectSchema.getProperties(); + //then: + assertFalse(properties.containsKey("value")); + } + + @Test + public void shouldAddStringIdToTheNamePropertyOfTestBeanClass() { + //when: + StringSchema stringSchema = jsonSchema.asObjectSchema() + .getProperties() + .get("name") + .asStringSchema(); + //then: + assertEquals(EXPECTED_NAME_ID, stringSchema.getId()); + } + + @Test + public void shouldNotAddStringIdToTheLastNamePropertyOfTestBeanClass() { + //when: + StringSchema stringSchema = jsonSchema.asObjectSchema() + .getProperties() + .get("lastName") + .asStringSchema(); + //then: + assertNull(stringSchema.getId()); + } +}