From 0daa0905bbb39527ebb3d9f1323cbbd300c9c319 Mon Sep 17 00:00:00 2001 From: Carsten Wickner <11309681+CarstenWickner@users.noreply.github.com> Date: Wed, 15 Nov 2023 21:54:31 +0100 Subject: [PATCH] chore: further refactoring for code health improvement (#406) * chore: further refactor maven plugin * chore: refactoring of generation context * feat: add cache for types with members * chore: reduce GlobHandler complexity --- checkstyle.xml | 2 +- .../jsonschema/generator/MethodScope.java | 44 ++-- .../jsonschema/generator/SchemaBuilder.java | 26 ++- .../generator/SchemaGeneratorConfig.java | 5 +- .../jsonschema/generator/TypeContext.java | 13 ++ .../impl/SchemaGenerationContextImpl.java | 152 +++++++------ .../jsonschema/generator/impl/Util.java | 90 ++++++++ .../jsonschema/plugin/maven/GlobHandler.java | 210 +++++++++++------- .../plugin/maven/SchemaGeneratorMojo.java | 119 ++++------ .../plugin/maven/GlobHandlerTest.java | 2 +- .../module/jackson/JacksonModule.java | 36 +-- .../module/jackson/JsonSubTypesResolver.java | 173 ++++++++++----- .../validation/JakartaValidationModule.java | 32 +-- 13 files changed, 560 insertions(+), 344 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..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 @@ -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 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 + 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)); + } + } + /** * 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/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); } 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..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 @@ -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,27 @@ private void collectObjectProperties(ResolvedType targetType, Map> targetProperties, Set requiredProperties) { + ResolvedType hierarchyType = singleHierarchy.getType(); + logger.debug("collecting static fields and methods from {}", hierarchyType); + ResolvedTypeWithMembers hierarchyTypeMembers = this.typeContext.resolveWithMembers(hierarchyType); + if (this.generatorConfig.shouldIncludeStaticFields()) { + this.collectFields(hierarchyTypeMembers, ResolvedTypeWithMembers::getStaticFields, targetProperties, requiredProperties); + } + if (this.generatorConfig.shouldIncludeStaticMethods()) { + this.collectMethods(hierarchyTypeMembers, ResolvedTypeWithMembers::getStaticMethods, targetProperties, requiredProperties); + } + } + /** * Preparation Step: add the designated fields to the specified {@link Map}. * @@ -622,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() @@ -703,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() @@ -793,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()) { 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..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 @@ -16,14 +16,31 @@ package com.github.victools.jsonschema.plugin.maven; +import java.util.Arrays; +import java.util.List; +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. */ public class GlobHandler { + 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 int[] GLOB_IDENTIFIERS = { + ESCAPE_CHAR, ASTERISK_CHAR, QUESTION_MARK_CHAR, '/', '+', '[', '{' + }; + private static final int[] INPUT_CHARS_REQUIRING_ESCAPE = { + '.', '(', ')', '+', '|', '^', '$', '@', '%' + }; + /** * Generate predicate to check the given input for filtering classes on the classpath. * @@ -43,14 +60,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 +68,15 @@ public static Pattern createClassOrPackageNamePattern(String input, boolean forP return Pattern.compile(inputRegex); } + private static String convertInputToRegex(String input) { + if (IntStream.of(GLOB_IDENTIFIERS).anyMatch(identifier -> input.chars().anyMatch(inputChar -> inputChar == identifier))) { + // 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,99 +90,126 @@ 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; - sb.append('['); + handleOpeningBracketChar(sb, inClass, firstIndexInClass, index); break; case ']': - inClass--; - sb.append(']'); + handleClosingBracketChar(sb, inClass); break; - case '.': - case '(': - case ')': - case '+': - case '|': - case '^': - case '$': - case '@': - case '%': - if (inClass == 0 || (firstIndexInClass == i && ch == '^')) { - sb.append('\\'); - } - sb.append(ch); - break; - case '!': - if (firstIndexInClass == i) { - sb.append('^'); - } else { - sb.append('!'); - } + case EXCLAMATION_SIGN_CHAR: + handleExclamationSignChar(sb, firstIndexInClass, index); break; case '{': - inGroup++; - sb.append('('); + handleOpeningBraceChar(sb, inGroup); break; case '}': - inGroup--; - sb.append(')'); + handleClosingBraceChar(sb, inGroup); break; - case ',': - if (inGroup > 0) { - sb.append('|'); - } else { - sb.append(','); - } + case COMMA_CHAR: + handleCommaChar(sb, inGroup); break; default: + 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); + } sb.append(ch); } } 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 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('|'); + } 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 0b1610bd..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; @@ -56,7 +56,6 @@ 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; @@ -182,24 +181,15 @@ 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 : Util.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 : Util.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 (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); } @@ -229,16 +219,7 @@ 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); } } @@ -255,6 +236,20 @@ private void generateSchema(Class schemaClass) throws MojoExecutionException this.writeToFile(jsonSchema, file); } + private void logForNoClassesMatchingFilter(String classOrPackageName) throws MojoExecutionException { + StringBuilder message = new StringBuilder("No matching class found for \"") + .append(classOrPackageName) + .append("\" on classpath"); + if (!Util.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()); + } + /** * Get all the names of classes on the classpath. * @@ -295,29 +290,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 = Util.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); - } + Util.nullSafe(this.classNames).stream() + .map(className -> GlobHandler.createClassOrPackageNameFilter(className, false)) + .forEach(inclusions::add); + Util.nullSafe(this.packageNames).stream() + .map(packageName -> GlobHandler.createClassOrPackageNameFilter(packageName, true)) + .forEach(inclusions::add); } return element -> { String classPathEntry = element.getName().replaceAll("\\.", "/"); @@ -428,21 +414,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 + Util.nullSafe(this.options.enabled).forEach(configBuilder::with); + // Disable all the configured options + Util.nullSafe(this.options.disabled).forEach(configBuilder::without); } } @@ -454,13 +430,10 @@ private void setOptions(SchemaGeneratorConfigBuilder configBuilder) { */ @SuppressWarnings("unchecked") private void setModules(SchemaGeneratorConfigBuilder configBuilder) throws MojoExecutionException { - if (this.modules == null) { - return; - } - for (GeneratorModule module : 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); } } @@ -534,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-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"), 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..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 @@ -50,8 +50,10 @@ 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.function.ToIntBiFunction; import java.util.stream.Stream; /** @@ -511,21 +513,23 @@ 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); - } + this.overrideMapPropertyCountAttribute(memberAttributes, context.getKeyword(SchemaKeyword.TAG_PROPERTIES_MIN), + this.resolveMapMinEntries(member), Math::min); + this.overrideMapPropertyCountAttribute(memberAttributes, context.getKeyword(SchemaKeyword.TAG_PROPERTIES_MAX), + this.resolveMapMaxEntries(member), Math::max); + } + + private void overrideMapPropertyCountAttribute(ObjectNode memberAttributes, String attribute, Integer newValue, + ToIntBiFunction getStricterValue) { + if (newValue == null) { + return; } - 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); - } + JsonNode existingValue = memberAttributes.get(attribute); + boolean shouldSetNewValue = existingValue == null + || !existingValue.isNumber() + || newValue == getStricterValue.applyAsInt(newValue, existingValue.asInt()); + if (shouldSetNewValue) { + memberAttributes.put(attribute, newValue); } } }