From ca5c04647fc292420e3eae878759b461a5a82893 Mon Sep 17 00:00:00 2001 From: Carsten Wickner Date: Sun, 12 Nov 2023 14:24:27 +0100 Subject: [PATCH 01/13] chore: further refactor maven plugin --- .../plugin/maven/SchemaGeneratorMojo.java | 116 ++++++++---------- 1 file changed, 53 insertions(+), 63 deletions(-) diff --git a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java index 0b1610bd..158141c7 100644 --- a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java +++ b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java @@ -51,6 +51,7 @@ import java.nio.file.Files; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -182,29 +183,35 @@ public synchronized void execute() throws MojoExecutionException { // trigger initialization of the generator instance this.getGenerator(); - if (this.classNames != null) { - for (String className : this.classNames) { - this.getLog().info("Generating JSON Schema for " + className + ""); - this.generateSchema(className, false); - } + for (String className : nullSafe(this.classNames)) { + this.getLog().info("Generating JSON Schema for " + className + ""); + this.generateSchema(className, false); } - - if (this.packageNames != null) { - for (String packageName : this.packageNames) { - this.getLog().info("Generating JSON Schema for " + packageName + ""); - this.generateSchema(packageName, true); - } + for (String packageName : nullSafe(this.packageNames)) { + this.getLog().info("Generating JSON Schema for " + packageName + ""); + this.generateSchema(packageName, true); } - - boolean classAndPackageEmpty = (this.classNames == null || this.classNames.length == 0) - && (this.packageNames == null || this.packageNames.length == 0); - - if (classAndPackageEmpty && this.annotations != null && !this.annotations.isEmpty()) { + if (isNullOrEmpty(this.classNames) && isNullOrEmpty(this.packageNames) && !isNullOrEmpty(this.annotations)) { this.getLog().info("Generating JSON Schema for all annotated classes"); this.generateSchema("**/*", false); } } + private static boolean isNullOrEmpty(Object[] array) { + return array == null || array.length == 0; + } + + private static boolean isNullOrEmpty(List list) { + return list == null || list.isEmpty(); + } + + private static List nullSafe(T[] array) { + if (isNullOrEmpty(array)) { + return Collections.emptyList(); + } + return Arrays.asList(array); + } + /** * Generate the JSON schema for the given className. * @@ -229,19 +236,24 @@ private void generateSchema(String classOrPackageName, boolean targetPackage) th } } if (matchingClasses.isEmpty()) { - StringBuilder message = new StringBuilder("No matching class found for \"") - .append(classOrPackageName) - .append("\" on classpath"); - if (this.excludeClassNames != null && this.excludeClassNames.length > 0) { - message.append(" that wasn't excluded"); - } - if (this.failIfNoClassesMatch) { - throw new MojoExecutionException(message.toString()); - } - this.getLog().warn(message.toString()); + this.logForNoClassesMatchingFilter(classOrPackageName); } } + private void logForNoClassesMatchingFilter(String classOrPackageName) throws MojoExecutionException { + StringBuilder message = new StringBuilder("No matching class found for \"") + .append(classOrPackageName) + .append("\" on classpath"); + if (!isNullOrEmpty(this.excludeClassNames)) { + message.append(" that wasn't excluded"); + } + if (this.failIfNoClassesMatch) { + message.append(".\nYou can change this error to a warning by setting: false"); + throw new MojoExecutionException(message.toString()); + } + this.getLog().warn(message.toString()); + } + /** * Generate the JSON schema for the given className. * @@ -295,29 +307,20 @@ private List getAllClassNames() { * @return filter instance to apply on a ClassInfoList containing possibly eligible classpath elements */ private ClassInfoList.ClassInfoFilter createClassInfoFilter(boolean considerAnnotations) { - Set> exclusions; - if (this.excludeClassNames == null || this.excludeClassNames.length == 0) { - exclusions = Collections.emptySet(); - } else { - exclusions = Stream.of(this.excludeClassNames) - .map(excludeEntry -> GlobHandler.createClassOrPackageNameFilter(excludeEntry, false)) - .collect(Collectors.toSet()); - } + Set> exclusions = nullSafe(this.excludeClassNames).stream() + .map(excludeEntry -> GlobHandler.createClassOrPackageNameFilter(excludeEntry, false)) + .collect(Collectors.toSet()); Set> inclusions; if (considerAnnotations) { inclusions = Collections.singleton(input -> true); } else { inclusions = new HashSet<>(); - if (this.classNames != null) { - Stream.of(this.classNames) - .map(className -> GlobHandler.createClassOrPackageNameFilter(className, false)) - .forEach(inclusions::add); - } - if (this.packageNames != null) { - Stream.of(this.packageNames) - .map(packageName -> GlobHandler.createClassOrPackageNameFilter(packageName, true)) - .forEach(inclusions::add); - } + nullSafe(this.classNames).stream() + .map(className -> GlobHandler.createClassOrPackageNameFilter(className, false)) + .forEach(inclusions::add); + nullSafe(this.packageNames).stream() + .map(packageName -> GlobHandler.createClassOrPackageNameFilter(packageName, true)) + .forEach(inclusions::add); } return element -> { String classPathEntry = element.getName().replaceAll("\\.", "/"); @@ -428,21 +431,11 @@ private OptionPreset getOptionPreset() { * @param configBuilder The configbuilder on which the options are set */ private void setOptions(SchemaGeneratorConfigBuilder configBuilder) { - if (this.options == null) { - return; - } - // Enable all the configured options - if (this.options.enabled != null) { - for (Option option : this.options.enabled) { - configBuilder.with(option); - } - } - - // Disable all the configured options - if (this.options.disabled != null) { - for (Option option : this.options.disabled) { - configBuilder.without(option); - } + if (this.options != null) { + // Enable all the configured options + nullSafe(this.options.enabled).forEach(configBuilder::with); + // Disable all the configured options + nullSafe(this.options.disabled).forEach(configBuilder::without); } } @@ -454,10 +447,7 @@ private void setOptions(SchemaGeneratorConfigBuilder configBuilder) { */ @SuppressWarnings("unchecked") private void setModules(SchemaGeneratorConfigBuilder configBuilder) throws MojoExecutionException { - if (this.modules == null) { - return; - } - for (GeneratorModule module : this.modules) { + for (GeneratorModule module : nullSafe(this.modules)) { if (module.className != null && !module.className.isEmpty()) { this.addCustomModule(module.className, configBuilder); } else if (module.name != null) { From a584e243c85d38532707c66b7e161d30e249745b Mon Sep 17 00:00:00 2001 From: Carsten Wickner Date: Sun, 12 Nov 2023 20:12:38 +0100 Subject: [PATCH 02/13] chore: move new method to comply with checkstyle rules --- .../plugin/maven/SchemaGeneratorMojo.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java index 158141c7..17f3d5c8 100644 --- a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java +++ b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java @@ -240,6 +240,19 @@ private void generateSchema(String classOrPackageName, boolean targetPackage) th } } + /** + * Generate the JSON schema for the given className. + * + * @param schemaClass The class for which the schema is to be generated + * @throws MojoExecutionException In case of problems + */ + private void generateSchema(Class schemaClass) throws MojoExecutionException { + JsonNode jsonSchema = getGenerator().generateSchema(schemaClass); + File file = getSchemaFile(schemaClass); + this.getLog().info("- Writing schema to file: " + file); + this.writeToFile(jsonSchema, file); + } + private void logForNoClassesMatchingFilter(String classOrPackageName) throws MojoExecutionException { StringBuilder message = new StringBuilder("No matching class found for \"") .append(classOrPackageName) @@ -254,19 +267,6 @@ private void logForNoClassesMatchingFilter(String classOrPackageName) throws Moj this.getLog().warn(message.toString()); } - /** - * Generate the JSON schema for the given className. - * - * @param schemaClass The class for which the schema is to be generated - * @throws MojoExecutionException In case of problems - */ - private void generateSchema(Class schemaClass) throws MojoExecutionException { - JsonNode jsonSchema = getGenerator().generateSchema(schemaClass); - File file = getSchemaFile(schemaClass); - this.getLog().info("- Writing schema to file: " + file); - this.writeToFile(jsonSchema, file); - } - /** * Get all the names of classes on the classpath. * From c4ebc1fdf16332657614ff8cf4699d15743bbd1a Mon Sep 17 00:00:00 2001 From: Carsten Wickner Date: Mon, 13 Nov 2023 00:22:07 +0100 Subject: [PATCH 03/13] chore: partial refactoring of generation context --- .../impl/SchemaGenerationContextImpl.java | 157 ++++++++++-------- 1 file changed, 87 insertions(+), 70 deletions(-) diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java index 0641916f..14d28c6f 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java @@ -37,6 +37,7 @@ import com.github.victools.jsonschema.generator.SchemaKeyword; import com.github.victools.jsonschema.generator.TypeContext; import com.github.victools.jsonschema.generator.TypeScope; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -173,6 +174,10 @@ public Set getDefinedTypes() { */ public SchemaGenerationContextImpl addReference(ResolvedType javaType, ObjectNode referencingNode, CustomDefinitionProviderV2 ignoredDefinitionProvider, boolean isNullable) { + if (referencingNode == null) { + // referencingNode should only be null for the main class for which the schema is being generated + return this; + } Map> targetMap = isNullable ? this.nullableReferences : this.references; DefinitionKey key = new DefinitionKey(javaType, ignoredDefinitionProvider); List valueList = targetMap.computeIfAbsent(key, k -> new ArrayList<>()); @@ -294,31 +299,22 @@ private void traverseGenericType(TypeScope scope, ObjectNode targetNode, boolean // nothing more to be done return; } - final ObjectNode definition; - final boolean includeTypeAttributes; + final Map.Entry definitionAndTypeAttributeInclusionFlag; final CustomDefinition customDefinition = this.generatorConfig.getCustomDefinition(targetType, this, ignoredDefinitionProvider); - if (customDefinition != null && (customDefinition.isMeantToBeInline() || forceInlineDefinition)) { - includeTypeAttributes = customDefinition.shouldIncludeAttributes(); - definition = applyInlineCustomDefinition(customDefinition, targetType, targetNode, isNullable, ignoredDefinitionProvider); + if (customDefinition == null) { + // always inline array types + boolean shouldInlineDefinition = forceInlineDefinition || this.typeContext.isContainerType(targetType) && targetNode != null; + definitionAndTypeAttributeInclusionFlag = applyStandardDefinition(shouldInlineDefinition, scope, targetNode, isNullable, + ignoredDefinitionProvider); + } else if (customDefinition.isMeantToBeInline() || forceInlineDefinition) { + definitionAndTypeAttributeInclusionFlag = applyInlineCustomDefinition(customDefinition, targetType, targetNode, isNullable, + ignoredDefinitionProvider); } else { - boolean isContainerType = this.typeContext.isContainerType(targetType); - boolean shouldInlineDefinition = forceInlineDefinition || isContainerType && targetNode != null && customDefinition == null; - definition = applyReferenceDefinition(shouldInlineDefinition, targetType, targetNode, isNullable, ignoredDefinitionProvider); - if (customDefinition != null) { - this.markDefinitionAsNeverInlinedIfRequired(customDefinition, targetType, ignoredDefinitionProvider); - logger.debug("applying configured custom definition for {}", targetType); - definition.setAll(customDefinition.getValue()); - includeTypeAttributes = customDefinition.shouldIncludeAttributes(); - } else if (isContainerType) { - logger.debug("generating array definition for {}", targetType); - this.generateArrayDefinition(scope, definition, isNullable); - includeTypeAttributes = true; - } else { - logger.debug("generating definition for {}", targetType); - includeTypeAttributes = !this.addSubtypeReferencesInDefinition(targetType, definition); - } + definitionAndTypeAttributeInclusionFlag = applyReferencedCustomDefinition(customDefinition, targetType, targetNode, isNullable, + ignoredDefinitionProvider); } - if (includeTypeAttributes) { + final ObjectNode definition = definitionAndTypeAttributeInclusionFlag.getKey(); + if (definitionAndTypeAttributeInclusionFlag.getValue()) { Set allowedSchemaTypes = this.collectAllowedSchemaTypes(definition); ObjectNode typeAttributes = AttributeCollector.collectTypeAttributes(scope, this, allowedSchemaTypes); // ensure no existing attributes in the 'definition' are replaced, by way of first overriding any conflicts the other way around @@ -331,8 +327,8 @@ private void traverseGenericType(TypeScope scope, ObjectNode targetNode, boolean .forEach(override -> override.overrideTypeAttributes(definition, scope, this)); } - private ObjectNode applyInlineCustomDefinition(CustomDefinition customDefinition, ResolvedType targetType, ObjectNode targetNode, - boolean isNullable, CustomDefinitionProviderV2 ignoredDefinitionProvider) { + private Map.Entry applyInlineCustomDefinition(CustomDefinition customDefinition, ResolvedType targetType, + ObjectNode targetNode, boolean isNullable, CustomDefinitionProviderV2 ignoredDefinitionProvider) { final ObjectNode definition; if (targetNode == null) { logger.debug("storing configured custom inline type for {} as definition (since it is the main schema \"#\")", targetType); @@ -347,24 +343,41 @@ private ObjectNode applyInlineCustomDefinition(CustomDefinition customDefinition if (isNullable) { this.makeNullable(definition); } - return definition; + return new AbstractMap.SimpleEntry<>(definition, customDefinition.shouldIncludeAttributes()); } - private ObjectNode applyReferenceDefinition(boolean shouldInlineDefinition, ResolvedType targetType, ObjectNode targetNode, boolean isNullable, - CustomDefinitionProviderV2 ignoredDefinitionProvider) { + private Map.Entry applyReferencedCustomDefinition(CustomDefinition customDefinition, ResolvedType targetType, + ObjectNode targetNode, boolean isNullable, CustomDefinitionProviderV2 ignoredDefinitionProvider) { + ObjectNode definition = this.generatorConfig.createObjectNode(); + this.putDefinition(targetType, definition, ignoredDefinitionProvider); + this.addReference(targetType, targetNode, ignoredDefinitionProvider, isNullable); + this.markDefinitionAsNeverInlinedIfRequired(customDefinition, targetType, ignoredDefinitionProvider); + logger.debug("applying configured custom definition for {}", targetType); + definition.setAll(customDefinition.getValue()); + return new AbstractMap.SimpleEntry<>(definition, customDefinition.shouldIncludeAttributes()); + } + + private Map.Entry applyStandardDefinition(boolean shouldInlineDefinition, TypeScope scope, ObjectNode targetNode, + boolean isNullable, CustomDefinitionProviderV2 ignoredDefinitionProvider) { + ResolvedType targetType = scope.getType(); final ObjectNode definition; if (shouldInlineDefinition) { - // always inline array types definition = targetNode; } else { definition = this.generatorConfig.createObjectNode(); this.putDefinition(targetType, definition, ignoredDefinitionProvider); - if (targetNode != null) { - // targetNode is only null for the main class for which the schema is being generated - this.addReference(targetType, targetNode, ignoredDefinitionProvider, isNullable); - } + this.addReference(targetType, targetNode, ignoredDefinitionProvider, isNullable); } - return definition; + final boolean includeTypeAttributes; + if (this.typeContext.isContainerType(targetType)) { + logger.debug("generating array definition for {}", targetType); + this.generateArrayDefinition(scope, definition, isNullable); + includeTypeAttributes = true; + } else { + logger.debug("generating definition for {}", targetType); + includeTypeAttributes = !this.addSubtypeReferencesInDefinition(targetType, definition); + } + return new AbstractMap.SimpleEntry<>(definition, includeTypeAttributes); } /** @@ -423,22 +436,20 @@ private void generateArrayDefinition(TypeScope targetScope, ObjectNode definitio if (isNullable) { this.extendTypeDeclarationToIncludeNull(definition); } - if (targetScope instanceof MemberScope && !((MemberScope) targetScope).isFakeContainerItemScope()) { - MemberScope fakeArrayItemMember = ((MemberScope) targetScope).asFakeContainerItemScope(); - JsonNode fakeItemDefinition; - if (targetScope instanceof FieldScope) { - fakeItemDefinition = this.populateFieldSchema((FieldScope) fakeArrayItemMember); - } else if (targetScope instanceof MethodScope) { - fakeItemDefinition = this.populateMethodSchema((MethodScope) fakeArrayItemMember); - } else { - throw new IllegalStateException("Unsupported member type: " + targetScope.getClass().getName()); - } - definition.set(this.getKeyword(SchemaKeyword.TAG_ITEMS), fakeItemDefinition); + definition.set(this.getKeyword(SchemaKeyword.TAG_ITEMS), this.populateItemMemberSchema(targetScope)); + } + + private JsonNode populateItemMemberSchema(TypeScope targetScope) { + JsonNode arrayItemDefinition; + if (targetScope instanceof FieldScope && !((FieldScope) targetScope).isFakeContainerItemScope()) { + arrayItemDefinition = this.populateFieldSchema(((FieldScope) targetScope).asFakeContainerItemScope()); + } else if (targetScope instanceof MethodScope && !((MethodScope) targetScope).isFakeContainerItemScope()) { + arrayItemDefinition = this.populateMethodSchema(((MethodScope) targetScope).asFakeContainerItemScope()); } else { - ObjectNode arrayItemTypeRef = this.generatorConfig.createObjectNode(); - definition.set(this.getKeyword(SchemaKeyword.TAG_ITEMS), arrayItemTypeRef); - this.traverseGenericType(targetScope.getContainerItemType(), arrayItemTypeRef, false); + arrayItemDefinition = this.generatorConfig.createObjectNode(); + this.traverseGenericType(targetScope.getContainerItemType(), (ObjectNode) arrayItemDefinition, false); } + return arrayItemDefinition; } /** @@ -521,35 +532,41 @@ private void collectObjectProperties(ResolvedType targetType, Map> targetProperties, Set requiredProperties) { + ResolvedType hierarchyType = singleHierarchy.getType(); + logger.debug("collecting static fields and methods from {}", hierarchyType); + boolean includeStaticFields = this.generatorConfig.shouldIncludeStaticFields(); + boolean includeStaticMethods = this.generatorConfig.shouldIncludeStaticMethods(); + boolean anyRelevantMembersPresent = (includeStaticFields && !hierarchyType.getStaticFields().isEmpty()) + || (includeStaticMethods && !hierarchyType.getStaticMethods().isEmpty()); + if (!anyRelevantMembersPresent) { + // no static members to look-up for this (super) type + return; + } + final ResolvedTypeWithMembers hierarchyTypeMembers; + if (hierarchyType == targetType) { + // avoid looking up the main type again + hierarchyTypeMembers = targetTypeWithMembers; + } else { + hierarchyTypeMembers = this.typeContext.resolveWithMembers(hierarchyType); + } + if (includeStaticFields) { + this.collectFields(hierarchyTypeMembers, ResolvedTypeWithMembers::getStaticFields, targetProperties, requiredProperties); + } + if (includeStaticMethods) { + this.collectMethods(hierarchyTypeMembers, ResolvedTypeWithMembers::getStaticMethods, targetProperties, requiredProperties); + } + } + /** * Preparation Step: add the designated fields to the specified {@link Map}. * From f4a43752c06968652ac12222268c62cd17bf9c29 Mon Sep 17 00:00:00 2001 From: Carsten Wickner Date: Mon, 13 Nov 2023 08:18:27 +0100 Subject: [PATCH 04/13] feat: add cache for types with members --- .../victools/jsonschema/generator/TypeContext.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java index c8ffebb8..d9ab6581 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java @@ -31,6 +31,7 @@ import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.WeakHashMap; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -44,6 +45,7 @@ public class TypeContext { private final TypeResolver typeResolver; private final MemberResolver memberResolver; + private final WeakHashMap typesWithMembersCache; private final AnnotationConfiguration annotationConfig; private final boolean derivingFieldsFromArgumentFreeMethods; @@ -87,6 +89,7 @@ private TypeContext(AnnotationConfiguration annotationConfig, boolean derivingFi this.memberResolver = new MemberResolver(this.typeResolver); this.annotationConfig = annotationConfig; this.derivingFieldsFromArgumentFreeMethods = derivingFieldsFromArgumentFreeMethods; + this.typesWithMembersCache = new WeakHashMap<>(); } /** @@ -129,6 +132,16 @@ public final ResolvedType resolveSubtype(ResolvedType supertype, Class subtyp * @return collection of (resolved) fields and methods */ public final ResolvedTypeWithMembers resolveWithMembers(ResolvedType resolvedType) { + return this.typesWithMembersCache.computeIfAbsent(resolvedType, this::resolveWithMembersForCache); + } + + /** + * Collect a given type's declared fields and methods for the inclusion in the internal cache. + * + * @param resolvedType type for which to collect declared fields and methods + * @return collection of (resolved) fields and methods + */ + private ResolvedTypeWithMembers resolveWithMembersForCache(ResolvedType resolvedType) { return this.memberResolver.resolve(resolvedType, this.annotationConfig, null); } From 2a89d63a34a5c91d1a5802afd1f14b7215b09c14 Mon Sep 17 00:00:00 2001 From: Carsten Wickner Date: Mon, 13 Nov 2023 08:19:49 +0100 Subject: [PATCH 05/13] chore: refactor static member collection metthod --- .../impl/SchemaGenerationContextImpl.java | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java index 14d28c6f..6b552dcf 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java @@ -535,34 +535,20 @@ private void collectObjectProperties(ResolvedType targetType, Map> targetProperties, Set requiredProperties) { ResolvedType hierarchyType = singleHierarchy.getType(); logger.debug("collecting static fields and methods from {}", hierarchyType); - boolean includeStaticFields = this.generatorConfig.shouldIncludeStaticFields(); - boolean includeStaticMethods = this.generatorConfig.shouldIncludeStaticMethods(); - boolean anyRelevantMembersPresent = (includeStaticFields && !hierarchyType.getStaticFields().isEmpty()) - || (includeStaticMethods && !hierarchyType.getStaticMethods().isEmpty()); - if (!anyRelevantMembersPresent) { - // no static members to look-up for this (super) type - return; - } - final ResolvedTypeWithMembers hierarchyTypeMembers; - if (hierarchyType == targetType) { - // avoid looking up the main type again - hierarchyTypeMembers = targetTypeWithMembers; - } else { - hierarchyTypeMembers = this.typeContext.resolveWithMembers(hierarchyType); - } - if (includeStaticFields) { + ResolvedTypeWithMembers hierarchyTypeMembers = this.typeContext.resolveWithMembers(hierarchyType); + if (this.generatorConfig.shouldIncludeStaticFields()) { this.collectFields(hierarchyTypeMembers, ResolvedTypeWithMembers::getStaticFields, targetProperties, requiredProperties); } - if (includeStaticMethods) { + if (this.generatorConfig.shouldIncludeStaticMethods()) { this.collectMethods(hierarchyTypeMembers, ResolvedTypeWithMembers::getStaticMethods, targetProperties, requiredProperties); } } From b30046353f025692ca09b4ed9f0846ac1abfd2af Mon Sep 17 00:00:00 2001 From: Carsten Wickner Date: Mon, 13 Nov 2023 21:18:22 +0100 Subject: [PATCH 06/13] chore: extract condition into named variable --- .../generator/impl/SchemaGenerationContextImpl.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java index 6b552dcf..eb5d2d0a 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java @@ -796,8 +796,9 @@ private JsonNode createMethodSchema(MethodScope method, boolean isNullable, bool boolean forceInlineDefinition, ObjectNode collectedAttributes, CustomDefinition customDefinition) { // create an "allOf" wrapper for the attributes related to this particular field and its general type final ObjectNode referenceContainer; - if (customDefinition != null && !customDefinition.shouldIncludeAttributes() - || collectedAttributes == null || collectedAttributes.isEmpty()) { + boolean ignoreCollectedAttributes = customDefinition != null && !customDefinition.shouldIncludeAttributes() + || collectedAttributes == null || collectedAttributes.isEmpty(); + if (ignoreCollectedAttributes) { // no need for the allOf, can use the sub-schema instance directly as reference referenceContainer = targetNode; } else if (customDefinition == null && scope.isContainerType()) { From 721b4c495b743e78d032f6b58869573bd07d5678 Mon Sep 17 00:00:00 2001 From: Carsten Wickner Date: Tue, 14 Nov 2023 23:14:10 +0100 Subject: [PATCH 07/13] chore: further reduction of cyclomatic complexity --- checkstyle.xml | 2 +- .../jsonschema/generator/MethodScope.java | 44 +++-- .../jsonschema/generator/SchemaBuilder.java | 26 ++- .../generator/SchemaGeneratorConfig.java | 5 +- .../impl/SchemaGenerationContextImpl.java | 4 +- .../jsonschema/generator/impl/Util.java | 90 +++++++++ .../jsonschema/plugin/maven/GlobHandler.java | 161 +++++++++------- .../plugin/maven/SchemaGeneratorMojo.java | 55 ++---- .../module/jackson/JacksonModule.java | 36 ++-- .../module/jackson/JsonSubTypesResolver.java | 173 ++++++++++++------ .../validation/JakartaValidationModule.java | 27 ++- 11 files changed, 405 insertions(+), 218 deletions(-) create mode 100644 jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/Util.java diff --git a/checkstyle.xml b/checkstyle.xml index f9e8fe47..c9b3956b 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -197,7 +197,7 @@ - + diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MethodScope.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MethodScope.java index 0d92bdf1..3268fbfb 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MethodScope.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MethodScope.java @@ -133,25 +133,9 @@ private FieldScope doFindGetterField() { String methodName = this.getDeclaredName(); Set possibleFieldNames = new HashSet<>(3); if (methodName.startsWith("get")) { - if (methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3))) { - // ensure that the variable starts with a lower-case letter - possibleFieldNames.add(methodName.substring(3, 4).toLowerCase() + methodName.substring(4)); - } - // @since 4.32.0 - conforming with JavaBeans API specification edge case when second character in field name is in uppercase - if (methodName.length() > 4 && Character.isUpperCase(methodName.charAt(4))) { - possibleFieldNames.add(methodName.substring(3)); - } + getPossibleFieldNamesStartingWithGet(methodName, possibleFieldNames); } else if (methodName.startsWith("is")) { - if (methodName.length() > 2 && Character.isUpperCase(methodName.charAt(2))) { - // ensure that the variable starts with a lower-case letter - possibleFieldNames.add(methodName.substring(2, 3).toLowerCase() + methodName.substring(3)); - // since 4.32.0: a method "isBool()" is considered a possible getter for a field "isBool" as well as for "bool" - possibleFieldNames.add(methodName); - } - // @since 4.32.0 - conforming with JavaBeans API specification edge case when second character in field name is in uppercase - if (methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3))) { - possibleFieldNames.add(methodName.substring(2)); - } + getPossibleFieldNamesStartingWithIs(methodName, possibleFieldNames); } if (possibleFieldNames.isEmpty()) { // method name does not fall into getter conventions @@ -166,6 +150,30 @@ private FieldScope doFindGetterField() { .orElse(null); } + private static void getPossibleFieldNamesStartingWithIs(String methodName, Set possibleFieldNames) { + if (methodName.length() > 2 && Character.isUpperCase(methodName.charAt(2))) { + // ensure that the variable starts with a lower-case letter + possibleFieldNames.add(methodName.substring(2, 3).toLowerCase() + methodName.substring(3)); + // since 4.32.0: a method "isBool()" is considered a possible getter for a field "isBool" as well as for "bool" + possibleFieldNames.add(methodName); + } + // @since 4.32.0 - conforming with JavaBeans API specification edge case when second character in field name is in uppercase + if (methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3))) { + possibleFieldNames.add(methodName.substring(2)); + } + } + + private static void getPossibleFieldNamesStartingWithGet(String methodName, Set possibleFieldNames) { + if (methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3))) { + // ensure that the variable starts with a lower-case letter + possibleFieldNames.add(methodName.substring(3, 4).toLowerCase() + methodName.substring(4)); + } + // @since 4.32.0 - conforming with JavaBeans API specification edge case when second character in field name is in uppercase + if (methodName.length() > 4 && Character.isUpperCase(methodName.charAt(4))) { + possibleFieldNames.add(methodName.substring(3)); + } + } + /** * Determine whether the method's name matches the getter naming convention ("getFoo()"/"isFoo()") and a respective field ("foo") exists. * diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaBuilder.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaBuilder.java index eca93d7c..2574309a 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaBuilder.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaBuilder.java @@ -34,6 +34,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collectors; /** @@ -212,7 +213,6 @@ private ObjectNode buildDefinitionsAndResolveReferences(String designatedDefinit createDefinitionsForAll, inlineAllSchemas); Map baseReferenceKeys = this.getReferenceKeys(mainSchemaKey, shouldProduceDefinition, generationContext); considerOnlyDirectReferences.set(true); - final boolean createDefinitionForMainSchema = this.config.shouldCreateDefinitionForMainSchema(); for (Map.Entry entry : baseReferenceKeys.entrySet()) { String definitionName = entry.getValue(); DefinitionKey definitionKey = entry.getKey(); @@ -227,13 +227,11 @@ private ObjectNode buildDefinitionsAndResolveReferences(String designatedDefinit referenceKey = null; } else { // the same sub-schema is referenced in multiple places - if (createDefinitionForMainSchema || !definitionKey.equals(mainSchemaKey)) { - // add it to the definitions (unless it is the main schema that is not explicitly moved there via an Option) - definitionsNode.set(definitionName, generationContext.getDefinition(definitionKey)); - referenceKey = this.config.getKeyword(SchemaKeyword.TAG_REF_MAIN) + '/' + designatedDefinitionPath + '/' + definitionName; - } else { - referenceKey = this.config.getKeyword(SchemaKeyword.TAG_REF_MAIN); - } + Supplier addDefinitionAndReturnReferenceKey = () -> { + definitionsNode.set(definitionName, this.generationContext.getDefinition(definitionKey)); + return this.config.getKeyword(SchemaKeyword.TAG_REF_MAIN) + '/' + designatedDefinitionPath + '/' + definitionName; + }; + referenceKey = getReferenceKey(mainSchemaKey, definitionKey, addDefinitionAndReturnReferenceKey); references.forEach(node -> node.put(this.config.getKeyword(SchemaKeyword.TAG_REF), referenceKey)); } if (!nullableReferences.isEmpty()) { @@ -260,6 +258,18 @@ private ObjectNode buildDefinitionsAndResolveReferences(String designatedDefinit return definitionsNode; } + private String getReferenceKey(DefinitionKey mainSchemaKey, DefinitionKey definitionKey, Supplier addDefinitionAndReturnReferenceKey) { + final String referenceKey; + if (definitionKey.equals(mainSchemaKey) && !this.config.shouldCreateDefinitionForMainSchema()) { + // no need to add the main schema into the definitions, unless explicitly configured to do so + referenceKey = this.config.getKeyword(SchemaKeyword.TAG_REF_MAIN); + } else { + // add it to the definitions + referenceKey = addDefinitionAndReturnReferenceKey.get(); + } + return referenceKey; + } + /** * Produce reusable predicate for checking whether a given type should produce an entry in the {@link SchemaKeyword#TAG_DEFINITIONS} or not. * diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java index 2007c69f..d2753fba 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.victools.jsonschema.generator.impl.Util; import com.github.victools.jsonschema.generator.naming.SchemaDefinitionNamingStrategy; import java.lang.annotation.Annotation; import java.math.BigDecimal; @@ -361,7 +362,7 @@ CustomDefinition getCustomDefinition(ResolvedType javaType, SchemaGenerationCont @Deprecated default ResolvedType resolveTargetTypeOverride(FieldScope field) { List result = this.resolveTargetTypeOverrides(field); - return result == null || result.isEmpty() ? null : result.get(0); + return Util.isNullOrEmpty(result) ? null : result.get(0); } /** @@ -374,7 +375,7 @@ default ResolvedType resolveTargetTypeOverride(FieldScope field) { @Deprecated default ResolvedType resolveTargetTypeOverride(MethodScope method) { List result = this.resolveTargetTypeOverrides(method); - return result == null || result.isEmpty() ? null : result.get(0); + return Util.isNullOrEmpty(result) ? null : result.get(0); } /** diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java index eb5d2d0a..de9d2c82 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java @@ -625,7 +625,7 @@ private ObjectNode populateFieldSchema(FieldScope field) { typeOverrides = this.generatorConfig.resolveSubtypes(field.getType(), this); } List fieldOptions; - if (typeOverrides == null || typeOverrides.isEmpty()) { + if (Util.isNullOrEmpty(typeOverrides)) { fieldOptions = Collections.singletonList(field); } else { fieldOptions = typeOverrides.stream() @@ -706,7 +706,7 @@ private JsonNode populateMethodSchema(MethodScope method) { typeOverrides = this.generatorConfig.resolveSubtypes(method.getType(), this); } List methodOptions; - if (typeOverrides == null || typeOverrides.isEmpty()) { + if (Util.isNullOrEmpty(typeOverrides)) { methodOptions = Collections.singletonList(method); } else { methodOptions = typeOverrides.stream() diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/Util.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/Util.java new file mode 100644 index 00000000..96aba642 --- /dev/null +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/Util.java @@ -0,0 +1,90 @@ +/* + * Copyright 2023 VicTools. + * + * 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 com.github.victools.jsonschema.generator.impl; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Utility class offering various helper functions to simplify common checks, e.g., with the goal reduce the complexity of checks and conditions. + */ +public final class Util { + + private Util() { + // private constructor to avoid instantiation + } + + /** + * Check whether the given text value is either {@code null} or empty (i.e., has zero length). + * + * @param string the text value to check + * @return check result + */ + public static boolean isNullOrEmpty(String string) { + return string == null || string.isEmpty(); + } + + /** + * Check whether the given array is either {@code null} or empty (i.e., has zero length). + * + * @param array the array to check + * @return check result + */ + public static boolean isNullOrEmpty(Object[] array) { + return array == null || array.length == 0; + } + + /** + * Check whether the given collection is either {@code null} or empty (i.e., has zero size). + * + * @param collection the collection to check + * @return check result + */ + public static boolean isNullOrEmpty(Collection collection) { + return collection == null || collection.isEmpty(); + } + + /** + * Convert the given array into a {@code List} containing its items. If the given array is {@code null}, an empty {@code List} is being returned. + * + * @param type of array items + * @param array the array to convert (may be {@code null} + * @return list instance + */ + public static List nullSafe(T[] array) { + if (isNullOrEmpty(array)) { + return Collections.emptyList(); + } + return Arrays.asList(array); + } + + /** + * Ensure the given list into a {@code List} containing its items. If the given array is {@code null}, an empty {@code List} is being returned. + * + * @param type of list items + * @param list the list to convert (may be {@code null} + * @return non-{@code null} list instance + */ + public static List nullSafe(List list) { + if (list == null) { + return Collections.emptyList(); + } + return list; + } +} diff --git a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java index 3212f0f0..7589a806 100644 --- a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java +++ b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java @@ -16,6 +16,7 @@ package com.github.victools.jsonschema.plugin.maven; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -24,6 +25,12 @@ */ public class GlobHandler { + public static final char ESCAPE_CHAR = '\\'; + public static final char ASTERISK_CHAR = '*'; + public static final char QUESTION_MARK_CHAR = '?'; + public static final char EXCLAMATION_SIGN_CHAR = '!'; + public static final char COMMA_CHAR = ','; + /** * Generate predicate to check the given input for filtering classes on the classpath. * @@ -43,14 +50,7 @@ public static Predicate createClassOrPackageNameFilter(String input, boo * @return regular expression to filter classes on classpath by */ public static Pattern createClassOrPackageNamePattern(String input, boolean forPackage) { - String inputRegex; - if (input.chars().anyMatch(c -> c == '/' || c == '*' || c == '?' || c == '+' || c == '[' || c == '{' || c == '\\')) { - // convert glob pattern into regular expression - inputRegex = GlobHandler.convertGlobToRegex(input); - } else { - // backward compatible support for absolute paths with "." as separator - inputRegex = input.replace('.', '/'); - } + String inputRegex = convertInputToRegex(input); if (forPackage) { // cater for any classname and any subpackages in between inputRegex += inputRegex.charAt(inputRegex.length() - 1) == '/' ? ".+" : "/.+"; @@ -58,6 +58,15 @@ public static Pattern createClassOrPackageNamePattern(String input, boolean forP return Pattern.compile(inputRegex); } + private static String convertInputToRegex(String input) { + if (input.chars().anyMatch(c -> c == '/' || c == '*' || c == '?' || c == '+' || c == '[' || c == '{' || c == ESCAPE_CHAR)) { + // convert glob pattern into regular expression + return GlobHandler.convertGlobToRegex(input); + } + // backward compatible support for absolute paths with "." as separator + return input.replace('.', '/'); + } + /** * Converts a standard POSIX Shell globbing pattern into a regular expression pattern. The result can be used with the standard * {@link java.util.regex} API to recognize strings which match the glob pattern. @@ -71,57 +80,29 @@ public static Pattern createClassOrPackageNamePattern(String input, boolean forP */ private static String convertGlobToRegex(String pattern) { StringBuilder sb = new StringBuilder(pattern.length()); - int inGroup = 0; - int inClass = 0; - int firstIndexInClass = -1; + AtomicInteger inGroup = new AtomicInteger(0); + AtomicInteger inClass = new AtomicInteger(0); + AtomicInteger firstIndexInClass = new AtomicInteger(-1); char[] arr = pattern.toCharArray(); - for (int i = 0; i < arr.length; i++) { - char ch = arr[i]; + for (AtomicInteger index = new AtomicInteger(0); index.get() < arr.length; index.incrementAndGet()) { + char ch = arr[index.get()]; switch (ch) { - case '\\': - if (++i >= arr.length) { - sb.append('\\'); - } else { - char next = arr[i]; - switch (next) { - case ',': - // escape not needed - break; - case 'Q': - case 'E': - // extra escape needed - sb.append("\\\\"); - break; - default: - sb.append('\\'); - } - sb.append(next); - } + case ESCAPE_CHAR: + handleEscapeChar(sb, arr, index.incrementAndGet()); break; - case '*': - if (inClass != 0) { - sb.append('*'); - } else if ((i + 1) < arr.length && arr[i + 1] == '*') { - i++; - sb.append(".*"); - } else { - sb.append("[^/]*"); - } + case ASTERISK_CHAR: + handleAsteriskChar(sb, inClass, arr, index); break; - case '?': - if (inClass == 0) { - sb.append("[^/]"); - } else { - sb.append('?'); - } + case QUESTION_MARK_CHAR: + handleQuestionMarkChar(sb, inClass); break; case '[': - inClass++; - firstIndexInClass = i + 1; + inClass.incrementAndGet(); + firstIndexInClass.set(index.get() + 1); sb.append('['); break; case ']': - inClass--; + inClass.decrementAndGet(); sb.append(']'); break; case '.': @@ -133,32 +114,24 @@ private static String convertGlobToRegex(String pattern) { case '$': case '@': case '%': - if (inClass == 0 || (firstIndexInClass == i && ch == '^')) { - sb.append('\\'); + if (inClass.get() == 0 || (firstIndexInClass.get() == index.get() && ch == '^')) { + sb.append(ESCAPE_CHAR); } sb.append(ch); break; - case '!': - if (firstIndexInClass == i) { - sb.append('^'); - } else { - sb.append('!'); - } + case EXCLAMATION_SIGN_CHAR: + handleExclamationSignChar(sb, firstIndexInClass, index); break; case '{': - inGroup++; + inGroup.incrementAndGet(); sb.append('('); break; case '}': - inGroup--; + inGroup.decrementAndGet(); sb.append(')'); break; - case ',': - if (inGroup > 0) { - sb.append('|'); - } else { - sb.append(','); - } + case COMMA_CHAR: + handleCommaChar(sb, inGroup); break; default: sb.append(ch); @@ -166,4 +139,60 @@ private static String convertGlobToRegex(String pattern) { } return sb.toString(); } + + private static void handleEscapeChar(StringBuilder sb, char[] arr, int nextCharIndex) { + if (nextCharIndex >= arr.length) { + sb.append(ESCAPE_CHAR); + } else { + char next = arr[nextCharIndex]; + switch (next) { + case COMMA_CHAR: + // escape not needed + break; + case 'Q': + case 'E': + // extra escape needed + sb.append(ESCAPE_CHAR).append(ESCAPE_CHAR); + break; + default: + sb.append(ESCAPE_CHAR); + } + sb.append(next); + } + } + + private static void handleAsteriskChar(StringBuilder sb, AtomicInteger inClass, char[] arr, AtomicInteger index) { + if (inClass.get() != 0) { + sb.append(ASTERISK_CHAR); + } else if ((index.get() + 1) < arr.length && arr[index.get() + 1] == ASTERISK_CHAR) { + index.incrementAndGet(); + sb.append(".*"); + } else { + sb.append("[^/]*"); + } + } + + private static void handleQuestionMarkChar(StringBuilder sb, AtomicInteger inClass) { + if (inClass.get() == 0) { + sb.append("[^/]"); + } else { + sb.append(QUESTION_MARK_CHAR); + } + } + + private static void handleExclamationSignChar(StringBuilder sb, AtomicInteger firstIndexInClass, AtomicInteger index) { + if (firstIndexInClass.get() == index.get()) { + sb.append('^'); + } else { + sb.append(EXCLAMATION_SIGN_CHAR); + } + } + + private static void handleCommaChar(StringBuilder sb, AtomicInteger inGroup) { + if (inGroup.get() > 0) { + sb.append('|'); + } else { + sb.append(COMMA_CHAR); + } + } } diff --git a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java index 17f3d5c8..64a05f7e 100644 --- a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java +++ b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java @@ -19,12 +19,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.victools.jsonschema.generator.Module; -import com.github.victools.jsonschema.generator.Option; import com.github.victools.jsonschema.generator.OptionPreset; import com.github.victools.jsonschema.generator.SchemaGenerator; import com.github.victools.jsonschema.generator.SchemaGeneratorConfig; import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; import com.github.victools.jsonschema.generator.SchemaVersion; +import com.github.victools.jsonschema.generator.impl.Util; import com.github.victools.jsonschema.module.jackson.JacksonModule; import com.github.victools.jsonschema.module.jackson.JacksonOption; import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationModule; @@ -51,13 +51,11 @@ import java.nio.file.Files; import java.text.MessageFormat; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Function; -import java.util.function.IntFunction; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -183,35 +181,20 @@ public synchronized void execute() throws MojoExecutionException { // trigger initialization of the generator instance this.getGenerator(); - for (String className : nullSafe(this.classNames)) { + for (String className : Util.nullSafe(this.classNames)) { this.getLog().info("Generating JSON Schema for " + className + ""); this.generateSchema(className, false); } - for (String packageName : nullSafe(this.packageNames)) { + for (String packageName : Util.nullSafe(this.packageNames)) { this.getLog().info("Generating JSON Schema for " + packageName + ""); this.generateSchema(packageName, true); } - if (isNullOrEmpty(this.classNames) && isNullOrEmpty(this.packageNames) && !isNullOrEmpty(this.annotations)) { + if (Util.isNullOrEmpty(this.classNames) && Util.isNullOrEmpty(this.packageNames) && !Util.isNullOrEmpty(this.annotations)) { this.getLog().info("Generating JSON Schema for all annotated classes"); this.generateSchema("**/*", false); } } - private static boolean isNullOrEmpty(Object[] array) { - return array == null || array.length == 0; - } - - private static boolean isNullOrEmpty(List list) { - return list == null || list.isEmpty(); - } - - private static List nullSafe(T[] array) { - if (isNullOrEmpty(array)) { - return Collections.emptyList(); - } - return Arrays.asList(array); - } - /** * Generate the JSON schema for the given className. * @@ -257,7 +240,7 @@ private void logForNoClassesMatchingFilter(String classOrPackageName) throws Moj StringBuilder message = new StringBuilder("No matching class found for \"") .append(classOrPackageName) .append("\" on classpath"); - if (!isNullOrEmpty(this.excludeClassNames)) { + if (!Util.isNullOrEmpty(this.excludeClassNames)) { message.append(" that wasn't excluded"); } if (this.failIfNoClassesMatch) { @@ -307,7 +290,7 @@ private List getAllClassNames() { * @return filter instance to apply on a ClassInfoList containing possibly eligible classpath elements */ private ClassInfoList.ClassInfoFilter createClassInfoFilter(boolean considerAnnotations) { - Set> exclusions = nullSafe(this.excludeClassNames).stream() + Set> exclusions = Util.nullSafe(this.excludeClassNames).stream() .map(excludeEntry -> GlobHandler.createClassOrPackageNameFilter(excludeEntry, false)) .collect(Collectors.toSet()); Set> inclusions; @@ -315,10 +298,10 @@ private ClassInfoList.ClassInfoFilter createClassInfoFilter(boolean considerAnno inclusions = Collections.singleton(input -> true); } else { inclusions = new HashSet<>(); - nullSafe(this.classNames).stream() + Util.nullSafe(this.classNames).stream() .map(className -> GlobHandler.createClassOrPackageNameFilter(className, false)) .forEach(inclusions::add); - nullSafe(this.packageNames).stream() + Util.nullSafe(this.packageNames).stream() .map(packageName -> GlobHandler.createClassOrPackageNameFilter(packageName, true)) .forEach(inclusions::add); } @@ -433,9 +416,9 @@ private OptionPreset getOptionPreset() { private void setOptions(SchemaGeneratorConfigBuilder configBuilder) { if (this.options != null) { // Enable all the configured options - nullSafe(this.options.enabled).forEach(configBuilder::with); + Util.nullSafe(this.options.enabled).forEach(configBuilder::with); // Disable all the configured options - nullSafe(this.options.disabled).forEach(configBuilder::without); + Util.nullSafe(this.options.disabled).forEach(configBuilder::without); } } @@ -447,10 +430,10 @@ private void setOptions(SchemaGeneratorConfigBuilder configBuilder) { */ @SuppressWarnings("unchecked") private void setModules(SchemaGeneratorConfigBuilder configBuilder) throws MojoExecutionException { - for (GeneratorModule module : nullSafe(this.modules)) { - if (module.className != null && !module.className.isEmpty()) { + for (GeneratorModule module : Util.nullSafe(this.modules)) { + if (!Util.isNullOrEmpty(module.className)) { this.addCustomModule(module.className, configBuilder); - } else if (module.name != null) { + } else if (!Util.isNullOrEmpty(module.name)) { this.addStandardModule(module, configBuilder); } } @@ -524,13 +507,11 @@ private void addStandardModule(GeneratorModule module, SchemaGeneratorConfigBuil private > void addStandardModuleWithOptions(GeneratorModule module, SchemaGeneratorConfigBuilder configBuilder, Function moduleConstructor, Class optionType) throws MojoExecutionException { Stream.Builder optionStream = Stream.builder(); - if (module.options != null && module.options.length > 0) { - for (String optionName : module.options) { - try { - optionStream.add(Enum.valueOf(optionType, optionName)); - } catch (IllegalArgumentException e) { - throw new MojoExecutionException("Error: Unknown " + module.name + " option " + optionName, e); - } + for (String optionName : Util.nullSafe(module.options)) { + try { + optionStream.add(Enum.valueOf(optionType, optionName)); + } catch (IllegalArgumentException e) { + throw new MojoExecutionException("Error: Unknown " + module.name + " option " + optionName, e); } } T[] options = optionStream.build().toArray(count -> (T[]) Array.newInstance(optionType, count)); diff --git a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JacksonModule.java b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JacksonModule.java index 0a4852eb..1d887605 100644 --- a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JacksonModule.java +++ b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JacksonModule.java @@ -112,25 +112,31 @@ public void applyToConfigBuilder(SchemaGeneratorConfigBuilder builder) { methodConfigPart.withCustomDefinitionProvider(identityReferenceDefinitionProvider::provideCustomPropertySchemaDefinition); } - boolean lookUpSubtypes = !this.options.contains(JacksonOption.SKIP_SUBTYPE_LOOKUP); - boolean includeTypeInfoTransform = !this.options.contains(JacksonOption.IGNORE_TYPE_INFO_TRANSFORM); - if (lookUpSubtypes || includeTypeInfoTransform) { - JsonSubTypesResolver subtypeResolver = new JsonSubTypesResolver(this.options); - if (lookUpSubtypes) { - generalConfigPart.withSubtypeResolver(subtypeResolver); - fieldConfigPart.withTargetTypeOverridesResolver(subtypeResolver::findTargetTypeOverrides); - methodConfigPart.withTargetTypeOverridesResolver(subtypeResolver::findTargetTypeOverrides); - } - if (includeTypeInfoTransform) { - generalConfigPart.withCustomDefinitionProvider(subtypeResolver); - fieldConfigPart.withCustomDefinitionProvider(subtypeResolver::provideCustomPropertySchemaDefinition); - methodConfigPart.withCustomDefinitionProvider(subtypeResolver::provideCustomPropertySchemaDefinition); - } - } + applySubtypeResolverToConfigBuilder(generalConfigPart, fieldConfigPart, methodConfigPart); generalConfigPart.withCustomDefinitionProvider(new JsonUnwrappedDefinitionProvider()); } + private void applySubtypeResolverToConfigBuilder(SchemaGeneratorGeneralConfigPart generalConfigPart, + SchemaGeneratorConfigPart fieldConfigPart, SchemaGeneratorConfigPart methodConfigPart) { + boolean skipLookUpSubtypes = this.options.contains(JacksonOption.SKIP_SUBTYPE_LOOKUP); + boolean skipTypeInfoTransform = this.options.contains(JacksonOption.IGNORE_TYPE_INFO_TRANSFORM); + if (skipLookUpSubtypes && skipTypeInfoTransform) { + return; + } + JsonSubTypesResolver subtypeResolver = new JsonSubTypesResolver(this.options); + if (!skipLookUpSubtypes) { + generalConfigPart.withSubtypeResolver(subtypeResolver); + fieldConfigPart.withTargetTypeOverridesResolver(subtypeResolver::findTargetTypeOverrides); + methodConfigPart.withTargetTypeOverridesResolver(subtypeResolver::findTargetTypeOverrides); + } + if (!skipTypeInfoTransform) { + generalConfigPart.withCustomDefinitionProvider(subtypeResolver); + fieldConfigPart.withCustomDefinitionProvider(subtypeResolver::provideCustomPropertySchemaDefinition); + methodConfigPart.withCustomDefinitionProvider(subtypeResolver::provideCustomPropertySchemaDefinition); + } + } + /** * Apply common member configurations. * diff --git a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolver.java b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolver.java index d864aec1..8aa92769 100644 --- a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolver.java +++ b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolver.java @@ -32,6 +32,7 @@ import com.github.victools.jsonschema.generator.SchemaKeyword; import com.github.victools.jsonschema.generator.SubtypeResolver; import com.github.victools.jsonschema.generator.TypeContext; +import com.github.victools.jsonschema.generator.TypeScope; import com.github.victools.jsonschema.generator.impl.AttributeCollector; import java.util.Collection; import java.util.Collections; @@ -174,7 +175,8 @@ public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, Sch Class erasedTypeWithTypeInfo = typeWithTypeInfo.getErasedType(); JsonTypeInfo typeInfoAnnotation = erasedTypeWithTypeInfo.getAnnotation(JsonTypeInfo.class); JsonSubTypes subTypesAnnotation = erasedTypeWithTypeInfo.getAnnotation(JsonSubTypes.class); - ObjectNode definition = this.createSubtypeDefinition(javaType, typeInfoAnnotation, subTypesAnnotation, null, context); + TypeScope scope = context.getTypeContext().createTypeScope(javaType); + ObjectNode definition = this.createSubtypeDefinition(scope, typeInfoAnnotation, subTypesAnnotation, context); if (definition == null) { return null; } @@ -204,16 +206,8 @@ public CustomPropertyDefinition provideCustomPropertySchemaDefinition(MemberScop .add(context.createStandardDefinitionReference(scope.getType(), this)); return new CustomPropertyDefinition(definition, CustomDefinition.AttributeInclusion.YES); } - ObjectNode attributes; - if (scope instanceof FieldScope) { - attributes = AttributeCollector.collectFieldAttributes((FieldScope) scope, context); - } else if (scope instanceof MethodScope) { - attributes = AttributeCollector.collectMethodAttributes((MethodScope) scope, context); - } else { - attributes = null; - } JsonSubTypes subTypesAnnotation = scope.getAnnotationConsideringFieldAndGetter(JsonSubTypes.class); - ObjectNode definition = this.createSubtypeDefinition(scope.getType(), typeInfoAnnotation, subTypesAnnotation, attributes, context); + ObjectNode definition = this.createSubtypeDefinition(scope, typeInfoAnnotation, subTypesAnnotation, context); if (definition == null) { return null; } @@ -293,68 +287,32 @@ private static String getUnqualifiedClassName(Class erasedTargetType) { /** * Create the custom schema definition for the given subtype, considering the {@link JsonTypeInfo#include()} setting. * - * @param javaType targeted subtype + * @param scope targeted subtype * @param typeInfoAnnotation annotation for looking up the type identifier and determining the kind of inclusion/serialization * @param subTypesAnnotation annotation specifying the mapping from super to subtypes (potentially including the discriminator values) - * @param attributesToInclude optional: additional attributes to include on the actual/contained schema definition * @param context generation context * @return created custom definition (or {@code null} if no supported subtype resolution scenario could be detected */ - private ObjectNode createSubtypeDefinition(ResolvedType javaType, JsonTypeInfo typeInfoAnnotation, JsonSubTypes subTypesAnnotation, - ObjectNode attributesToInclude, SchemaGenerationContext context) { + private ObjectNode createSubtypeDefinition(TypeScope scope, JsonTypeInfo typeInfoAnnotation, JsonSubTypes subTypesAnnotation, + SchemaGenerationContext context) { + ResolvedType javaType = scope.getType(); final String typeIdentifier = this.getTypeIdentifier(javaType, typeInfoAnnotation, subTypesAnnotation); if (typeIdentifier == null) { return null; } + ObjectNode attributesToInclude = this.getAttributesToInclude(scope, context); final ObjectNode definition = context.getGeneratorConfig().createObjectNode(); + SubtypeDefinitionDetails subtypeDetails = new SubtypeDefinitionDetails(javaType, attributesToInclude, context, typeIdentifier, definition); switch (typeInfoAnnotation.include()) { case WRAPPER_ARRAY: - definition.put(context.getKeyword(SchemaKeyword.TAG_TYPE), context.getKeyword(SchemaKeyword.TAG_TYPE_ARRAY)); - ArrayNode itemsArray = definition.withArray(context.getKeyword(SchemaKeyword.TAG_PREFIX_ITEMS)); - itemsArray.addObject() - .put(context.getKeyword(SchemaKeyword.TAG_TYPE), context.getKeyword(SchemaKeyword.TAG_TYPE_STRING)) - .put(context.getKeyword(SchemaKeyword.TAG_CONST), typeIdentifier); - if (attributesToInclude == null || attributesToInclude.isEmpty()) { - itemsArray.add(this.createNestedSubtypeSchema(javaType, context)); - } else { - itemsArray.addObject() - .withArray(context.getKeyword(SchemaKeyword.TAG_ALLOF)) - .add(this.createNestedSubtypeSchema(javaType, context)) - .add(attributesToInclude); - } + createSubtypeDefinitionForWrapperArrayTypeInfo(subtypeDetails); break; case WRAPPER_OBJECT: - definition.put(context.getKeyword(SchemaKeyword.TAG_TYPE), context.getKeyword(SchemaKeyword.TAG_TYPE_OBJECT)); - ObjectNode propertiesNode = definition.putObject(context.getKeyword(SchemaKeyword.TAG_PROPERTIES)); - if (attributesToInclude == null || attributesToInclude.isEmpty()) { - propertiesNode.set(typeIdentifier, this.createNestedSubtypeSchema(javaType, context)); - } else { - propertiesNode.putObject(typeIdentifier) - .withArray(context.getKeyword(SchemaKeyword.TAG_ALLOF)) - .add(this.createNestedSubtypeSchema(javaType, context)) - .add(attributesToInclude); - } - definition.withArray(context.getKeyword(SchemaKeyword.TAG_REQUIRED)).add(typeIdentifier); + this.createSubtypeDefinitionForWrapperObjectTypeInfo(subtypeDetails); break; case PROPERTY: case EXISTING_PROPERTY: - final String propertyName = Optional.ofNullable(typeInfoAnnotation.property()) - .filter(name -> !name.isEmpty()) - .orElseGet(() -> typeInfoAnnotation.use().getDefaultPropertyName()); - ObjectNode additionalPart = definition.withArray(context.getKeyword(SchemaKeyword.TAG_ALLOF)) - .add(this.createNestedSubtypeSchema(javaType, context)) - .addObject(); - if (attributesToInclude != null && !attributesToInclude.isEmpty()) { - additionalPart.setAll(attributesToInclude); - } - additionalPart.put(context.getKeyword(SchemaKeyword.TAG_TYPE), context.getKeyword(SchemaKeyword.TAG_TYPE_OBJECT)) - .putObject(context.getKeyword(SchemaKeyword.TAG_PROPERTIES)) - .putObject(propertyName) - .put(context.getKeyword(SchemaKeyword.TAG_CONST), typeIdentifier); - if (!javaType.getErasedType().equals(typeInfoAnnotation.defaultImpl())) { - additionalPart.withArray(context.getKeyword(SchemaKeyword.TAG_REQUIRED)) - .add(propertyName); - } + this.createSubtypeDefinitionForPropertyTypeInfo(subtypeDetails, typeInfoAnnotation); break; default: return null; @@ -362,10 +320,115 @@ private ObjectNode createSubtypeDefinition(ResolvedType javaType, JsonTypeInfo t return definition; } + private void createSubtypeDefinitionForWrapperArrayTypeInfo(SubtypeDefinitionDetails details) { + details.getDefinition().put(details.getKeyword(SchemaKeyword.TAG_TYPE), details.getKeyword(SchemaKeyword.TAG_TYPE_ARRAY)); + ArrayNode itemsArray = details.getDefinition().withArray(details.getKeyword(SchemaKeyword.TAG_PREFIX_ITEMS)); + itemsArray.addObject() + .put(details.getKeyword(SchemaKeyword.TAG_TYPE), details.getKeyword(SchemaKeyword.TAG_TYPE_STRING)) + .put(details.getKeyword(SchemaKeyword.TAG_CONST), details.getTypeIdentifier()); + if (details.getAttributesToInclude() == null || details.getAttributesToInclude().isEmpty()) { + itemsArray.add(this.createNestedSubtypeSchema(details.getJavaType(), details.getContext())); + } else { + itemsArray.addObject() + .withArray(details.getKeyword(SchemaKeyword.TAG_ALLOF)) + .add(this.createNestedSubtypeSchema(details.getJavaType(), details.getContext())) + .add(details.getAttributesToInclude()); + } + } + + private void createSubtypeDefinitionForWrapperObjectTypeInfo(SubtypeDefinitionDetails details) { + details.getDefinition().put(details.getKeyword(SchemaKeyword.TAG_TYPE), details.getKeyword(SchemaKeyword.TAG_TYPE_OBJECT)); + ObjectNode propertiesNode = details.getDefinition() + .putObject(details.getKeyword(SchemaKeyword.TAG_PROPERTIES)); + ObjectNode nestedSubtypeSchema = this.createNestedSubtypeSchema(details.getJavaType(), details.getContext()); + if (details.getAttributesToInclude() == null || details.getAttributesToInclude().isEmpty()) { + propertiesNode.set(details.getTypeIdentifier(), nestedSubtypeSchema); + } else { + propertiesNode.putObject(details.getTypeIdentifier()) + .withArray(details.getKeyword(SchemaKeyword.TAG_ALLOF)) + .add(nestedSubtypeSchema) + .add(details.getAttributesToInclude()); + } + details.getDefinition().withArray(details.getKeyword(SchemaKeyword.TAG_REQUIRED)).add(details.getTypeIdentifier()); + } + + private void createSubtypeDefinitionForPropertyTypeInfo(SubtypeDefinitionDetails details, JsonTypeInfo typeInfoAnnotation) { + final String propertyName = Optional.ofNullable(typeInfoAnnotation.property()) + .filter(name -> !name.isEmpty()) + .orElseGet(() -> typeInfoAnnotation.use().getDefaultPropertyName()); + ObjectNode additionalPart = details.getDefinition().withArray(details.getKeyword(SchemaKeyword.TAG_ALLOF)) + .add(this.createNestedSubtypeSchema(details.getJavaType(), details.getContext())) + .addObject(); + if (details.getAttributesToInclude() != null && !details.getAttributesToInclude().isEmpty()) { + additionalPart.setAll(details.getAttributesToInclude()); + } + additionalPart.put(details.getKeyword(SchemaKeyword.TAG_TYPE), details.getKeyword(SchemaKeyword.TAG_TYPE_OBJECT)) + .putObject(details.getKeyword(SchemaKeyword.TAG_PROPERTIES)) + .putObject(propertyName) + .put(details.getKeyword(SchemaKeyword.TAG_CONST), details.getTypeIdentifier()); + if (!details.getJavaType().getErasedType().equals(typeInfoAnnotation.defaultImpl())) { + additionalPart.withArray(details.getKeyword(SchemaKeyword.TAG_REQUIRED)) + .add(propertyName); + } + } + private ObjectNode createNestedSubtypeSchema(ResolvedType javaType, SchemaGenerationContext context) { if (this.shouldInlineNestedSubtypes) { return context.createStandardDefinition(javaType, this); } return context.createStandardDefinitionReference(javaType, this); } + + private ObjectNode getAttributesToInclude(TypeScope scope, SchemaGenerationContext context) { + ObjectNode attributesToInclude; + if (scope instanceof FieldScope) { + attributesToInclude = AttributeCollector.collectFieldAttributes((FieldScope) scope, context); + } else if (scope instanceof MethodScope) { + attributesToInclude = AttributeCollector.collectMethodAttributes((MethodScope) scope, context); + } else { + attributesToInclude = null; + } + return attributesToInclude; + } + + private static class SubtypeDefinitionDetails { + private final ResolvedType javaType; + private final ObjectNode attributesToInclude; + private final SchemaGenerationContext context; + private final String typeIdentifier; + private final ObjectNode definition; + + SubtypeDefinitionDetails(ResolvedType javaType, ObjectNode attributesToInclude, SchemaGenerationContext context, + String typeIdentifier, ObjectNode definition) { + this.javaType = javaType; + this.attributesToInclude = attributesToInclude; + this.context = context; + this.typeIdentifier = typeIdentifier; + this.definition = definition; + } + + ResolvedType getJavaType() { + return this.javaType; + } + + ObjectNode getAttributesToInclude() { + return this.attributesToInclude; + } + + SchemaGenerationContext getContext() { + return this.context; + } + + String getTypeIdentifier() { + return this.typeIdentifier; + } + + ObjectNode getDefinition() { + return this.definition; + } + + String getKeyword(SchemaKeyword keyword) { + return this.context.getKeyword(keyword); + } + } } diff --git a/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java b/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java index 4e1889b1..80ef4be0 100644 --- a/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java +++ b/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java @@ -50,6 +50,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.function.BiPredicate; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; @@ -511,20 +512,18 @@ protected void overrideInstanceAttributes(ObjectNode memberAttributes, MemberSco // in its current version, this instance attribute override is only considering Map types return; } - Integer mapMinEntries = this.resolveMapMinEntries(member); - if (mapMinEntries != null) { - String minPropertiesAttribute = context.getKeyword(SchemaKeyword.TAG_PROPERTIES_MIN); - JsonNode existingValue = memberAttributes.get(minPropertiesAttribute); - if (existingValue == null || (existingValue.isNumber() && existingValue.asInt() < mapMinEntries)) { - memberAttributes.put(minPropertiesAttribute, mapMinEntries); - } - } - Integer mapMaxEntries = this.resolveMapMaxEntries(member); - if (mapMaxEntries != null) { - String maxPropertiesAttribute = context.getKeyword(SchemaKeyword.TAG_PROPERTIES_MAX); - JsonNode existingValue = memberAttributes.get(maxPropertiesAttribute); - if (existingValue == null || (existingValue.isNumber() && existingValue.asInt() > mapMaxEntries)) { - memberAttributes.put(maxPropertiesAttribute, mapMaxEntries); + this.overrideMapPropertyCountAttribute(memberAttributes, context.getKeyword(SchemaKeyword.TAG_PROPERTIES_MIN), + this.resolveMapMinEntries(member), (newValue, existingValue) -> newValue > existingValue); + this.overrideMapPropertyCountAttribute(memberAttributes, context.getKeyword(SchemaKeyword.TAG_PROPERTIES_MAX), + this.resolveMapMaxEntries(member), (newValue, existingValue) -> newValue < existingValue); + } + + private void overrideMapPropertyCountAttribute(ObjectNode memberAttributes, String attribute, Integer newValue, + BiPredicate isOneStricterThanOther) { + if (newValue != null) { + JsonNode existingValue = memberAttributes.get(attribute); + if (existingValue == null || (existingValue.isNumber() && isOneStricterThanOther.test(newValue, existingValue.asInt()))) { + memberAttributes.put(attribute, newValue); } } } From a3243d1fe11c858bc0aed34fafb77358ad22ac42 Mon Sep 17 00:00:00 2001 From: Carsten Wickner Date: Tue, 14 Nov 2023 23:40:14 +0100 Subject: [PATCH 08/13] chore: reduce GlobHandler complexity --- .../jsonschema/plugin/maven/GlobHandler.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java index 7589a806..17ee7453 100644 --- a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java +++ b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java @@ -16,20 +16,28 @@ package com.github.victools.jsonschema.plugin.maven; +import java.util.Arrays; +import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.regex.Pattern; +import java.util.stream.Stream; /** * Conversion logic from globs to regular expressions. */ public class GlobHandler { - public static final char ESCAPE_CHAR = '\\'; - public static final char ASTERISK_CHAR = '*'; - public static final char QUESTION_MARK_CHAR = '?'; - public static final char EXCLAMATION_SIGN_CHAR = '!'; - public static final char COMMA_CHAR = ','; + private static final char ESCAPE_CHAR = '\\'; + private static final char ASTERISK_CHAR = '*'; + private static final char QUESTION_MARK_CHAR = '?'; + private static final char EXCLAMATION_SIGN_CHAR = '!'; + private static final char COMMA_CHAR = ','; + + private static final List GLOB_IDENTIFIERS = Arrays.asList( + ESCAPE_CHAR, ASTERISK_CHAR, QUESTION_MARK_CHAR, '/', '+', '[', '{' + ); /** * Generate predicate to check the given input for filtering classes on the classpath. @@ -59,7 +67,7 @@ public static Pattern createClassOrPackageNamePattern(String input, boolean forP } private static String convertInputToRegex(String input) { - if (input.chars().anyMatch(c -> c == '/' || c == '*' || c == '?' || c == '+' || c == '[' || c == '{' || c == ESCAPE_CHAR)) { + if (input.chars().anyMatch(GLOB_IDENTIFIERS::contains)) { // convert glob pattern into regular expression return GlobHandler.convertGlobToRegex(input); } From 0f8a84f19f1edc5a11b027dd76611eb0ccf6dd7a Mon Sep 17 00:00:00 2001 From: Carsten Wickner Date: Wed, 15 Nov 2023 00:10:14 +0100 Subject: [PATCH 09/13] chore: reduce GlobHandler switch case count --- .../jsonschema/plugin/maven/GlobHandler.java | 64 +++++++++++-------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java index 17ee7453..7f29089e 100644 --- a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java +++ b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java @@ -18,11 +18,9 @@ import java.util.Arrays; import java.util.List; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.regex.Pattern; -import java.util.stream.Stream; /** * Conversion logic from globs to regular expressions. @@ -38,6 +36,9 @@ public class GlobHandler { private static final List GLOB_IDENTIFIERS = Arrays.asList( ESCAPE_CHAR, ASTERISK_CHAR, QUESTION_MARK_CHAR, '/', '+', '[', '{' ); + private static final List INPUT_CHARS_REQUIRING_ESCAPE = Arrays.asList( + '.', '(', ')', '+', '|', '^', '$', '@', '%' + ); /** * Generate predicate to check the given input for filtering classes on the classpath. @@ -105,44 +106,25 @@ private static String convertGlobToRegex(String pattern) { handleQuestionMarkChar(sb, inClass); break; case '[': - inClass.incrementAndGet(); - firstIndexInClass.set(index.get() + 1); - sb.append('['); + handleOpeningBracketChar(sb, inClass, firstIndexInClass, index); break; case ']': - inClass.decrementAndGet(); - sb.append(']'); - break; - case '.': - case '(': - case ')': - case '+': - case '|': - case '^': - case '$': - case '@': - case '%': - if (inClass.get() == 0 || (firstIndexInClass.get() == index.get() && ch == '^')) { - sb.append(ESCAPE_CHAR); - } - sb.append(ch); + handleClosingBracketChar(sb, inClass); break; case EXCLAMATION_SIGN_CHAR: handleExclamationSignChar(sb, firstIndexInClass, index); break; case '{': - inGroup.incrementAndGet(); - sb.append('('); + handleOpeningBraceChar(sb, inGroup); break; case '}': - inGroup.decrementAndGet(); - sb.append(')'); + handleClosingBraceChar(sb, inGroup); break; case COMMA_CHAR: handleCommaChar(sb, inGroup); break; default: - sb.append(ch); + handleOtherChar(sb, ch, inClass, firstIndexInClass, index); } } return sb.toString(); @@ -196,6 +178,27 @@ private static void handleExclamationSignChar(StringBuilder sb, AtomicInteger fi } } + private static void handleOpeningBracketChar(StringBuilder sb, AtomicInteger inClass, AtomicInteger firstIndexInClass, AtomicInteger index) { + inClass.incrementAndGet(); + firstIndexInClass.set(index.get() + 1); + sb.append('['); + } + + private static void handleClosingBracketChar(StringBuilder sb, AtomicInteger inClass) { + inClass.decrementAndGet(); + sb.append(']'); + } + + private static void handleOpeningBraceChar(StringBuilder sb, AtomicInteger inGroup) { + inGroup.incrementAndGet(); + sb.append('('); + } + + private static void handleClosingBraceChar(StringBuilder sb, AtomicInteger inGroup) { + inGroup.decrementAndGet(); + sb.append(')'); + } + private static void handleCommaChar(StringBuilder sb, AtomicInteger inGroup) { if (inGroup.get() > 0) { sb.append('|'); @@ -203,4 +206,13 @@ private static void handleCommaChar(StringBuilder sb, AtomicInteger inGroup) { sb.append(COMMA_CHAR); } } + + private static void handleOtherChar(StringBuilder sb, char ch, AtomicInteger inClass, AtomicInteger firstIndexInClass, AtomicInteger index) { + boolean shouldBeEscaped = INPUT_CHARS_REQUIRING_ESCAPE.contains(ch) + && (inClass.get() == 0 || (ch == '^' && firstIndexInClass.get() == index.get())); + if (shouldBeEscaped) { + sb.append(ESCAPE_CHAR); + } + sb.append(ch); + } } From 0a94ae4ea734f8a21b84e0da593e0e19a18c966e Mon Sep 17 00:00:00 2001 From: Carsten Wickner Date: Wed, 15 Nov 2023 00:16:04 +0100 Subject: [PATCH 10/13] chore: avoid method with excess arguments --- .../jsonschema/plugin/maven/GlobHandler.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java index 7f29089e..e8c0bf12 100644 --- a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java +++ b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java @@ -124,7 +124,12 @@ private static String convertGlobToRegex(String pattern) { handleCommaChar(sb, inGroup); break; default: - handleOtherChar(sb, ch, inClass, firstIndexInClass, index); + boolean shouldBeEscaped = INPUT_CHARS_REQUIRING_ESCAPE.contains(ch) + && (inClass.get() == 0 || (ch == '^' && firstIndexInClass.get() == index.get())); + if (shouldBeEscaped) { + sb.append(ESCAPE_CHAR); + } + sb.append(ch); } } return sb.toString(); @@ -206,13 +211,4 @@ private static void handleCommaChar(StringBuilder sb, AtomicInteger inGroup) { sb.append(COMMA_CHAR); } } - - private static void handleOtherChar(StringBuilder sb, char ch, AtomicInteger inClass, AtomicInteger firstIndexInClass, AtomicInteger index) { - boolean shouldBeEscaped = INPUT_CHARS_REQUIRING_ESCAPE.contains(ch) - && (inClass.get() == 0 || (ch == '^' && firstIndexInClass.get() == index.get())); - if (shouldBeEscaped) { - sb.append(ESCAPE_CHAR); - } - sb.append(ch); - } } From c88c9c4fa428cdb203dcd6176716d3fc15a997ac Mon Sep 17 00:00:00 2001 From: Carsten Wickner Date: Wed, 15 Nov 2023 21:24:25 +0100 Subject: [PATCH 11/13] chore: fix refactoring regression --- .../jsonschema/plugin/maven/GlobHandler.java | 13 +++++++------ .../jsonschema/plugin/maven/GlobHandlerTest.java | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java index e8c0bf12..4b4b32bb 100644 --- a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java +++ b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java @@ -21,6 +21,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.regex.Pattern; +import java.util.stream.IntStream; /** * Conversion logic from globs to regular expressions. @@ -33,12 +34,12 @@ public class GlobHandler { private static final char EXCLAMATION_SIGN_CHAR = '!'; private static final char COMMA_CHAR = ','; - private static final List GLOB_IDENTIFIERS = Arrays.asList( + private static final int[] GLOB_IDENTIFIERS = { ESCAPE_CHAR, ASTERISK_CHAR, QUESTION_MARK_CHAR, '/', '+', '[', '{' - ); - private static final List INPUT_CHARS_REQUIRING_ESCAPE = Arrays.asList( + }; + private static final int[] INPUT_CHARS_REQUIRING_ESCAPE = { '.', '(', ')', '+', '|', '^', '$', '@', '%' - ); + }; /** * Generate predicate to check the given input for filtering classes on the classpath. @@ -68,7 +69,7 @@ public static Pattern createClassOrPackageNamePattern(String input, boolean forP } private static String convertInputToRegex(String input) { - if (input.chars().anyMatch(GLOB_IDENTIFIERS::contains)) { + if (IntStream.of(GLOB_IDENTIFIERS).anyMatch(identifier -> input.chars().anyMatch(inputChar -> inputChar == identifier))) { // convert glob pattern into regular expression return GlobHandler.convertGlobToRegex(input); } @@ -124,7 +125,7 @@ private static String convertGlobToRegex(String pattern) { handleCommaChar(sb, inGroup); break; default: - boolean shouldBeEscaped = INPUT_CHARS_REQUIRING_ESCAPE.contains(ch) + boolean shouldBeEscaped = IntStream.of(INPUT_CHARS_REQUIRING_ESCAPE).anyMatch(specialChar -> specialChar == ch) && (inClass.get() == 0 || (ch == '^' && firstIndexInClass.get() == index.get())); if (shouldBeEscaped) { sb.append(ESCAPE_CHAR); diff --git a/jsonschema-maven-plugin/src/test/java/com/github/victools/jsonschema/plugin/maven/GlobHandlerTest.java b/jsonschema-maven-plugin/src/test/java/com/github/victools/jsonschema/plugin/maven/GlobHandlerTest.java index 1c5f666e..635bb747 100644 --- a/jsonschema-maven-plugin/src/test/java/com/github/victools/jsonschema/plugin/maven/GlobHandlerTest.java +++ b/jsonschema-maven-plugin/src/test/java/com/github/victools/jsonschema/plugin/maven/GlobHandlerTest.java @@ -31,7 +31,7 @@ public class GlobHandlerTest { static Stream parametersForTestBasicPattern() { return Stream.of( - Arguments.of("single star becomes all-but-shlash star", "gl*b", "gl[^/]*b"), + Arguments.of("single star becomes all-but-slash star", "gl*b", "gl[^/]*b"), Arguments.of("double star becomes dot star", "gl**b", "gl.*b"), Arguments.of("escaped star is unchanged", "gl\\*b", "gl\\*b"), Arguments.of("question mark becomes all-but-shlash", "gl?b", "gl[^/]b"), From 86fe5dff1bd558e55e8e959b0ebb52e11fa38a31 Mon Sep 17 00:00:00 2001 From: Carsten Wickner Date: Wed, 15 Nov 2023 21:35:19 +0100 Subject: [PATCH 12/13] chore: swap methods for get and is to match call order --- .../jsonschema/generator/MethodScope.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MethodScope.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MethodScope.java index 3268fbfb..1a027a86 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MethodScope.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/MethodScope.java @@ -150,6 +150,17 @@ private FieldScope doFindGetterField() { .orElse(null); } + private static void getPossibleFieldNamesStartingWithGet(String methodName, Set possibleFieldNames) { + if (methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3))) { + // ensure that the variable starts with a lower-case letter + possibleFieldNames.add(methodName.substring(3, 4).toLowerCase() + methodName.substring(4)); + } + // @since 4.32.0 - conforming with JavaBeans API specification edge case when second character in field name is in uppercase + if (methodName.length() > 4 && Character.isUpperCase(methodName.charAt(4))) { + possibleFieldNames.add(methodName.substring(3)); + } + } + private static void getPossibleFieldNamesStartingWithIs(String methodName, Set possibleFieldNames) { if (methodName.length() > 2 && Character.isUpperCase(methodName.charAt(2))) { // ensure that the variable starts with a lower-case letter @@ -163,17 +174,6 @@ private static void getPossibleFieldNamesStartingWithIs(String methodName, Set possibleFieldNames) { - if (methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3))) { - // ensure that the variable starts with a lower-case letter - possibleFieldNames.add(methodName.substring(3, 4).toLowerCase() + methodName.substring(4)); - } - // @since 4.32.0 - conforming with JavaBeans API specification edge case when second character in field name is in uppercase - if (methodName.length() > 4 && Character.isUpperCase(methodName.charAt(4))) { - possibleFieldNames.add(methodName.substring(3)); - } - } - /** * Determine whether the method's name matches the getter naming convention ("getFoo()"/"isFoo()") and a respective field ("foo") exists. * From 664134cdfdcf3cb5695f4bf718f85c1520a1084f Mon Sep 17 00:00:00 2001 From: Carsten Wickner Date: Wed, 15 Nov 2023 21:50:09 +0100 Subject: [PATCH 13/13] chore: replace lambda with standard function --- .../validation/JakartaValidationModule.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java b/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java index 80ef4be0..3bfc2574 100644 --- a/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java +++ b/jsonschema-module-jakarta-validation/src/main/java/com/github/victools/jsonschema/module/jakarta/validation/JakartaValidationModule.java @@ -53,6 +53,7 @@ import java.util.function.BiPredicate; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.ToIntBiFunction; import java.util.stream.Stream; /** @@ -513,18 +514,22 @@ protected void overrideInstanceAttributes(ObjectNode memberAttributes, MemberSco return; } this.overrideMapPropertyCountAttribute(memberAttributes, context.getKeyword(SchemaKeyword.TAG_PROPERTIES_MIN), - this.resolveMapMinEntries(member), (newValue, existingValue) -> newValue > existingValue); + this.resolveMapMinEntries(member), Math::min); this.overrideMapPropertyCountAttribute(memberAttributes, context.getKeyword(SchemaKeyword.TAG_PROPERTIES_MAX), - this.resolveMapMaxEntries(member), (newValue, existingValue) -> newValue < existingValue); + this.resolveMapMaxEntries(member), Math::max); } private void overrideMapPropertyCountAttribute(ObjectNode memberAttributes, String attribute, Integer newValue, - BiPredicate isOneStricterThanOther) { - if (newValue != null) { - JsonNode existingValue = memberAttributes.get(attribute); - if (existingValue == null || (existingValue.isNumber() && isOneStricterThanOther.test(newValue, existingValue.asInt()))) { - memberAttributes.put(attribute, newValue); - } + ToIntBiFunction getStricterValue) { + if (newValue == null) { + return; + } + JsonNode existingValue = memberAttributes.get(attribute); + boolean shouldSetNewValue = existingValue == null + || !existingValue.isNumber() + || newValue == getStricterValue.applyAsInt(newValue, existingValue.asInt()); + if (shouldSetNewValue) { + memberAttributes.put(attribute, newValue); } } }