From 81470da6f6328a01d3c45fc36e45b91f726f55fd Mon Sep 17 00:00:00 2001 From: Carsten Wickner <11309681+CarstenWickner@users.noreply.github.com> Date: Fri, 10 Nov 2023 22:25:37 +0100 Subject: [PATCH] chore: improve code health by extracting methods (#402) --- .../impl/SchemaGenerationContextImpl.java | 305 ++++++++-------- .../plugin/maven/SchemaGeneratorMojo.java | 333 ++++++++---------- 2 files changed, 312 insertions(+), 326 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 f2a69086..0641916f 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 @@ -299,32 +299,11 @@ private void traverseGenericType(TypeScope scope, ObjectNode targetNode, boolean final CustomDefinition customDefinition = this.generatorConfig.getCustomDefinition(targetType, this, ignoredDefinitionProvider); if (customDefinition != null && (customDefinition.isMeantToBeInline() || forceInlineDefinition)) { includeTypeAttributes = customDefinition.shouldIncludeAttributes(); - if (targetNode == null) { - logger.debug("storing configured custom inline type for {} as definition (since it is the main schema \"#\")", targetType); - definition = customDefinition.getValue(); - this.putDefinition(targetType, definition, ignoredDefinitionProvider); - // targetNode will be populated at the end, in buildDefinitionsAndResolveReferences() - } else { - logger.debug("directly applying configured custom inline type for {}", targetType); - targetNode.setAll(customDefinition.getValue()); - definition = targetNode; - } - if (isNullable) { - this.makeNullable(definition); - } + definition = applyInlineCustomDefinition(customDefinition, targetType, targetNode, isNullable, ignoredDefinitionProvider); } else { boolean isContainerType = this.typeContext.isContainerType(targetType); - if (forceInlineDefinition || isContainerType && targetNode != null && customDefinition == null) { - // 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); - } - } + 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); @@ -352,6 +331,42 @@ 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) { + final ObjectNode definition; + if (targetNode == null) { + logger.debug("storing configured custom inline type for {} as definition (since it is the main schema \"#\")", targetType); + definition = customDefinition.getValue(); + this.putDefinition(targetType, definition, ignoredDefinitionProvider); + // targetNode will be populated at the end, in buildDefinitionsAndResolveReferences() + } else { + logger.debug("directly applying configured custom inline type for {}", targetType); + targetNode.setAll(customDefinition.getValue()); + definition = targetNode; + } + if (isNullable) { + this.makeNullable(definition); + } + return definition; + } + + private ObjectNode applyReferenceDefinition(boolean shouldInlineDefinition, ResolvedType targetType, ObjectNode targetNode, boolean isNullable, + CustomDefinitionProviderV2 ignoredDefinitionProvider) { + 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); + } + } + return definition; + } + /** * Check for any defined subtypes of the targeted java type to produce a definition for. If there are any configured subtypes, reference those * from within the definition being generated. @@ -404,13 +419,9 @@ private Set collectAllowedSchemaTypes(ObjectNode definition) { * @param isNullable whether the field/method's return value the targetType refers to is allowed to be null in the declaring type */ private void generateArrayDefinition(TypeScope targetScope, ObjectNode definition, boolean isNullable) { + definition.put(this.getKeyword(SchemaKeyword.TAG_TYPE), this.getKeyword(SchemaKeyword.TAG_TYPE_ARRAY)); if (isNullable) { - ArrayNode typeArray = this.generatorConfig.createArrayNode() - .add(this.getKeyword(SchemaKeyword.TAG_TYPE_ARRAY)) - .add(this.getKeyword(SchemaKeyword.TAG_TYPE_NULL)); - definition.set(this.getKeyword(SchemaKeyword.TAG_TYPE), typeArray); - } else { - definition.put(this.getKeyword(SchemaKeyword.TAG_TYPE), this.getKeyword(SchemaKeyword.TAG_TYPE_ARRAY)); + this.extendTypeDeclarationToIncludeNull(definition); } if (targetScope instanceof MemberScope && !((MemberScope) targetScope).isFakeContainerItemScope()) { MemberScope fakeArrayItemMember = ((MemberScope) targetScope).asFakeContainerItemScope(); @@ -448,46 +459,51 @@ private void generateObjectDefinition(ResolvedType targetType, ObjectNode defini this.collectObjectProperties(targetType, targetProperties, requiredProperties); - if (!targetProperties.isEmpty()) { - ObjectNode propertiesNode = this.generatorConfig.createObjectNode(); - List> sortedProperties = targetProperties.values().stream() - .sorted(this.generatorConfig::sortProperties) - .collect(Collectors.toList()); - Map> dependentRequires = new LinkedHashMap<>(); - for (MemberScope property : sortedProperties) { - JsonNode subSchema; - List dependentRequiredForProperty; - if (property instanceof FieldScope) { - subSchema = this.populateFieldSchema((FieldScope) property); - dependentRequiredForProperty = this.generatorConfig.resolveDependentRequires((FieldScope) property); - } else if (property instanceof MethodScope) { - subSchema = this.populateMethodSchema((MethodScope) property); - dependentRequiredForProperty = this.generatorConfig.resolveDependentRequires((MethodScope) property); - } else { - throw new IllegalStateException("Unsupported member scope of type " + property.getClass()); - } - String propertyName = property.getSchemaPropertyName(); - propertiesNode.set(propertyName, subSchema); - if (dependentRequiredForProperty != null && !dependentRequiredForProperty.isEmpty()) { - dependentRequires.put(propertyName, dependentRequiredForProperty); - } - } - definition.set(this.getKeyword(SchemaKeyword.TAG_PROPERTIES), propertiesNode); - if (!requiredProperties.isEmpty()) { - ArrayNode requiredNode = this.generatorConfig.createArrayNode(); - // list required properties in the same order as the property - sortedProperties.stream() - .map(MemberScope::getSchemaPropertyName) - .filter(requiredProperties::contains) - .forEach(requiredNode::add); - definition.set(this.getKeyword(SchemaKeyword.TAG_REQUIRED), requiredNode); - } - if (!dependentRequires.isEmpty()) { - ObjectNode dpendentRequiredNode = this.generatorConfig.createObjectNode(); - dependentRequires.forEach((leadName, dependentNames) -> dependentNames - .forEach(dpendentRequiredNode.withArray(leadName)::add)); - definition.set(this.getKeyword(SchemaKeyword.TAG_DEPENDENT_REQUIRED), dpendentRequiredNode); - } + if (targetProperties.isEmpty()) { + return; + } + ObjectNode propertiesNode = this.generatorConfig.createObjectNode(); + List> sortedProperties = targetProperties.values().stream() + .sorted(this.generatorConfig::sortProperties) + .collect(Collectors.toList()); + Map> dependentRequires = new LinkedHashMap<>(); + for (MemberScope property : sortedProperties) { + this.addPropertiesEntry(propertiesNode, dependentRequires, property); + } + definition.set(this.getKeyword(SchemaKeyword.TAG_PROPERTIES), propertiesNode); + if (!requiredProperties.isEmpty()) { + ArrayNode requiredNode = this.generatorConfig.createArrayNode(); + // list required properties in the same order as the property + sortedProperties.stream() + .map(MemberScope::getSchemaPropertyName) + .filter(requiredProperties::contains) + .forEach(requiredNode::add); + definition.set(this.getKeyword(SchemaKeyword.TAG_REQUIRED), requiredNode); + } + if (!dependentRequires.isEmpty()) { + ObjectNode dependentRequiredNode = this.generatorConfig.createObjectNode(); + dependentRequires.forEach((leadName, dependentNames) -> dependentNames + .forEach(dependentRequiredNode.withArray(leadName)::add)); + definition.set(this.getKeyword(SchemaKeyword.TAG_DEPENDENT_REQUIRED), dependentRequiredNode); + } + } + + private void addPropertiesEntry(ObjectNode propertiesNode, Map> dependentRequires, MemberScope property) { + JsonNode subSchema; + List dependentRequiredForProperty; + if (property instanceof FieldScope) { + subSchema = this.populateFieldSchema((FieldScope) property); + dependentRequiredForProperty = this.generatorConfig.resolveDependentRequires((FieldScope) property); + } else if (property instanceof MethodScope) { + subSchema = this.populateMethodSchema((MethodScope) property); + dependentRequiredForProperty = this.generatorConfig.resolveDependentRequires((MethodScope) property); + } else { + throw new IllegalStateException("Unsupported member scope of type " + property.getClass()); + } + String propertyName = property.getSchemaPropertyName(); + propertiesNode.set(propertyName, subSchema); + if (dependentRequiredForProperty != null && !dependentRequiredForProperty.isEmpty()) { + dependentRequires.put(propertyName, dependentRequiredForProperty); } } @@ -748,62 +764,70 @@ private JsonNode createMethodSchema(MethodScope method, boolean isNullable, bool ObjectNode collectedAttributes, CustomPropertyDefinitionProvider ignoredDefinitionProvider) { final CustomDefinition customDefinition = this.generatorConfig.getCustomDefinition(scope, this, ignoredDefinitionProvider); if (customDefinition != null && customDefinition.isMeantToBeInline()) { - if (customDefinition.getValue().isEmpty()) { - targetNode.withArray(this.getKeyword(SchemaKeyword.TAG_ALLOF)) - .add(customDefinition.getValue()); - } else { - targetNode.setAll(customDefinition.getValue()); - } - if (customDefinition.shouldIncludeAttributes()) { - AttributeCollector.mergeMissingAttributes(targetNode, collectedAttributes); - Set allowedSchemaTypes = this.collectAllowedSchemaTypes(targetNode); - ObjectNode typeAttributes = AttributeCollector.collectTypeAttributes(scope, this, allowedSchemaTypes); - AttributeCollector.mergeMissingAttributes(targetNode, typeAttributes); - } - if (isNullable) { - this.makeNullable(targetNode); - } + populateMemberSchemaWithInlineCustomDefinition(scope, targetNode, isNullable, collectedAttributes, customDefinition); } else { - // 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.size() == 0) { - // no need for the allOf, can use the sub-schema instance directly as reference - referenceContainer = targetNode; - } else if (customDefinition == null && scope.isContainerType()) { - // same as above, but the collected attributes should be applied also for containers/arrays - referenceContainer = targetNode; - AttributeCollector.mergeMissingAttributes(targetNode, collectedAttributes); - } else { - // avoid mixing potential "$ref" element with contextual attributes by introducing an "allOf" wrapper - // this is only relevant for DRAFT_7 and is being cleaned-up afterwards for newer DRAFT versions - referenceContainer = this.generatorConfig.createObjectNode(); - targetNode.set(this.getKeyword(SchemaKeyword.TAG_ALLOF), this.generatorConfig.createArrayNode() - .add(referenceContainer) - .add(collectedAttributes)); - } - // only add reference for separate definition if it is not a fixed type that should be in-lined - try { - this.traverseGenericType(scope, referenceContainer, isNullable, forceInlineDefinition, null); - } catch (UnsupportedOperationException ex) { - logger.warn("Skipping type definition due to error", ex); - } + populateMemberSchemaWithReference(scope, targetNode, isNullable, forceInlineDefinition, collectedAttributes, customDefinition); + } + } + + private > void populateMemberSchemaWithInlineCustomDefinition(M scope, ObjectNode targetNode, boolean isNullable, + ObjectNode collectedAttributes, CustomDefinition customDefinition) { + if (customDefinition.getValue().isEmpty()) { + targetNode.withArray(this.getKeyword(SchemaKeyword.TAG_ALLOF)) + .add(customDefinition.getValue()); + } else { + targetNode.setAll(customDefinition.getValue()); + } + if (customDefinition.shouldIncludeAttributes()) { + AttributeCollector.mergeMissingAttributes(targetNode, collectedAttributes); + Set allowedSchemaTypes = this.collectAllowedSchemaTypes(targetNode); + ObjectNode typeAttributes = AttributeCollector.collectTypeAttributes(scope, this, allowedSchemaTypes); + AttributeCollector.mergeMissingAttributes(targetNode, typeAttributes); + } + if (isNullable) { + this.makeNullable(targetNode); + } + } + + private > void populateMemberSchemaWithReference(M scope, ObjectNode targetNode, boolean isNullable, + 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()) { + // no need for the allOf, can use the sub-schema instance directly as reference + referenceContainer = targetNode; + } else if (customDefinition == null && scope.isContainerType()) { + // same as above, but the collected attributes should be applied also for containers/arrays + referenceContainer = targetNode; + AttributeCollector.mergeMissingAttributes(targetNode, collectedAttributes); + } else { + // avoid mixing potential "$ref" element with contextual attributes by introducing an "allOf" wrapper + // this is only relevant for DRAFT_6 / DRAFT_7 and is being cleaned-up afterward for newer schema versions + referenceContainer = this.generatorConfig.createObjectNode(); + targetNode.putArray(this.getKeyword(SchemaKeyword.TAG_ALLOF)) + .add(referenceContainer) + .add(collectedAttributes); + } + // only add reference for separate definition if it is not a fixed type that should be in-lined + try { + this.traverseGenericType(scope, referenceContainer, isNullable, forceInlineDefinition, null); + } catch (UnsupportedOperationException ex) { + logger.warn("Skipping type definition due to error", ex); } } @Override public ObjectNode makeNullable(ObjectNode node) { - final String nullTypeName = this.getKeyword(SchemaKeyword.TAG_TYPE_NULL); - if (node.has(this.getKeyword(SchemaKeyword.TAG_REF)) - || node.has(this.getKeyword(SchemaKeyword.TAG_ALLOF)) - || node.has(this.getKeyword(SchemaKeyword.TAG_ANYOF)) - || node.has(this.getKeyword(SchemaKeyword.TAG_ONEOF)) + Stream requiringAnyOfWrapper = Stream.of( + SchemaKeyword.TAG_REF, SchemaKeyword.TAG_ALLOF, SchemaKeyword.TAG_ANYOF, SchemaKeyword.TAG_ONEOF, // since version 4.21.0 - || node.has(this.getKeyword(SchemaKeyword.TAG_CONST)) - || node.has(this.getKeyword(SchemaKeyword.TAG_ENUM))) { + SchemaKeyword.TAG_CONST, SchemaKeyword.TAG_ENUM + ); + if (requiringAnyOfWrapper.map(this::getKeyword).anyMatch(node::has)) { // cannot be sure what is specified in those other schema parts, instead simply create an anyOf wrapper ObjectNode nullSchema = this.generatorConfig.createObjectNode() - .put(this.getKeyword(SchemaKeyword.TAG_TYPE), nullTypeName); + .put(this.getKeyword(SchemaKeyword.TAG_TYPE), this.getKeyword(SchemaKeyword.TAG_TYPE_NULL)); ArrayNode anyOf = this.generatorConfig.createArrayNode() // one option in the anyOf should be null .add(nullSchema) @@ -813,32 +837,35 @@ public ObjectNode makeNullable(ObjectNode node) { node.removeAll(); node.set(this.getKeyword(SchemaKeyword.TAG_ANYOF), anyOf); } else { - // given node is a simple schema, we can simply adjust its "type" attribute - JsonNode fixedJsonSchemaType = node.get(this.getKeyword(SchemaKeyword.TAG_TYPE)); - if (fixedJsonSchemaType instanceof ArrayNode) { - // there are already multiple "type" values - ArrayNode arrayOfTypes = (ArrayNode) fixedJsonSchemaType; - // one of the existing "type" values could be null - boolean alreadyContainsNull = false; - for (JsonNode arrayEntry : arrayOfTypes) { - alreadyContainsNull = alreadyContainsNull || nullTypeName.equals(arrayEntry.textValue()); - } + // given node is a simple schema, we can adjust its "type" attribute + this.extendTypeDeclarationToIncludeNull(node); + } + return node; + } - if (!alreadyContainsNull) { - // null "type" was not mentioned before, to be safe we need to replace the old array and add the null entry - node.replace(this.getKeyword(SchemaKeyword.TAG_TYPE), this.generatorConfig.createArrayNode() - .addAll(arrayOfTypes) - .add(nullTypeName)); + private void extendTypeDeclarationToIncludeNull(ObjectNode node) { + JsonNode fixedJsonSchemaType = node.get(this.getKeyword(SchemaKeyword.TAG_TYPE)); + final String nullTypeName = this.getKeyword(SchemaKeyword.TAG_TYPE_NULL); + if (fixedJsonSchemaType instanceof ArrayNode) { + // there are already multiple "type" values + ArrayNode arrayOfTypes = (ArrayNode) fixedJsonSchemaType; + // one of the existing "type" values could be null + for (JsonNode arrayEntry : arrayOfTypes) { + if (nullTypeName.equals(arrayEntry.textValue())) { + return; } - } else if (fixedJsonSchemaType instanceof TextNode && !nullTypeName.equals(fixedJsonSchemaType.textValue())) { - // add null as second "type" option - node.replace(this.getKeyword(SchemaKeyword.TAG_TYPE), this.generatorConfig.createArrayNode() - .add(fixedJsonSchemaType) - .add(nullTypeName)); } - // if no "type" is specified, null is allowed already + // null "type" was not mentioned before, to be safe we need to replace the old array and add the null entry + node.putArray(this.getKeyword(SchemaKeyword.TAG_TYPE)) + .addAll(arrayOfTypes) + .add(nullTypeName); + } else if (fixedJsonSchemaType instanceof TextNode && !nullTypeName.equals(fixedJsonSchemaType.textValue())) { + // add null as second "type" option + node.putArray(this.getKeyword(SchemaKeyword.TAG_TYPE)) + .add(fixedJsonSchemaType) + .add(nullTypeName); } - return node; + // if no "type" is specified, null is allowed already } @Override 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 ce90395d..0b1610bd 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 @@ -43,6 +43,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.net.URLClassLoader; @@ -54,6 +55,8 @@ 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; @@ -258,65 +261,76 @@ private void generateSchema(Class schemaClass) throws MojoExecutionException * @return A set of classes as found on the classpath, that are not explicitly excluded */ private List getAllClassNames() { - if (this.allTypes == null) { - ClassGraph classGraph = new ClassGraph() - .overrideClasspath(classpath.getClasspathElements(this.project)) - .enableClassInfo(); - boolean considerAnnotations = this.annotations != null && !this.annotations.isEmpty(); + if (this.allTypes != null) { + return this.allTypes; + } + ClassGraph classGraph = new ClassGraph() + .overrideClasspath(classpath.getClasspathElements(this.project)) + .enableClassInfo(); + boolean considerAnnotations = this.annotations != null && !this.annotations.isEmpty(); + if (considerAnnotations) { + classGraph.enableAnnotationInfo(); + } + ClassInfoList.ClassInfoFilter filter = createClassInfoFilter(considerAnnotations); + try (ScanResult scanResult = classGraph.scan()) { + Stream allTypesStream; if (considerAnnotations) { - classGraph.enableAnnotationInfo(); - } - Set> exclusions; - if (this.excludeClassNames == null || this.excludeClassNames.length == 0) { - exclusions = Collections.emptySet(); + allTypesStream = this.annotations.stream() + .flatMap(a -> scanResult.getClassesWithAnnotation(a.className).filter(filter).stream()) + .distinct(); } else { - exclusions = Stream.of(this.excludeClassNames) - .map(excludeEntry -> GlobHandler.createClassOrPackageNameFilter(excludeEntry, false)) - .collect(Collectors.toSet()); - } - Set> inclusions = new HashSet<>(); - if (considerAnnotations) { - inclusions.add(input -> true); - } else { - if (this.classNames != null) { - for (String className : this.classNames) { - inclusions.add(GlobHandler.createClassOrPackageNameFilter(className, false)); - } - } - if (this.packageNames != null) { - for (String packageName : this.packageNames) { - inclusions.add(GlobHandler.createClassOrPackageNameFilter(packageName, true)); - } - } + allTypesStream = scanResult.getAllClasses().filter(filter).stream(); } + this.allTypes = allTypesStream + .map(PotentialSchemaClass::new) + .collect(Collectors.toList()); + } + return this.allTypes; + } - ClassInfoList.ClassInfoFilter filter = element -> { - String classPathEntry = element.getName().replaceAll("\\.", "/"); - if (exclusions.stream().anyMatch(exclude -> exclude.test(classPathEntry))) { - this.getLog().debug(" Excluding: " + element.getName()); - return false; - } - if (inclusions.stream().anyMatch(include -> include.test(classPathEntry))) { - this.getLog().debug(" Including: " + element.getName()); - return true; - } - return false; - }; - Stream allTypesStream; - try (ScanResult scanResult = classGraph.scan()) { - if (considerAnnotations) { - allTypesStream = this.annotations.stream() - .flatMap(a -> scanResult.getClassesWithAnnotation(a.className).filter(filter).stream()) - .distinct(); - } else { - allTypesStream = scanResult.getAllClasses().filter(filter).stream(); - } - this.allTypes = allTypesStream - .map(PotentialSchemaClass::new) - .collect(Collectors.toList()); + /** + * Based on the plugin configuration, create a filter instance that determines whether a given classpath element should be considered. + * + * @param considerAnnotations whether the plugin configuration includes looking up types by certain annotations + * @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> 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); } } - return this.allTypes; + return element -> { + String classPathEntry = element.getName().replaceAll("\\.", "/"); + if (exclusions.stream().anyMatch(exclude -> exclude.test(classPathEntry))) { + this.getLog().debug(" Excluding: " + element.getName()); + return false; + } + if (inclusions.stream().anyMatch(include -> include.test(classPathEntry))) { + this.getLog().debug(" Including: " + element.getName()); + return true; + } + return false; + }; } /** @@ -445,177 +459,122 @@ private void setModules(SchemaGeneratorConfigBuilder configBuilder) throws MojoE } for (GeneratorModule module : this.modules) { if (module.className != null && !module.className.isEmpty()) { - try { - this.getLog().debug("- Adding custom Module " + module.className); - Class moduleClass = (Class) this.loadClass(module.className); - Module moduleInstance = moduleClass.getConstructor().newInstance(); - configBuilder.with(moduleInstance); - } catch (ClassCastException | InstantiationException - | IllegalAccessException | NoSuchMethodException - | InvocationTargetException e) { - throw new MojoExecutionException("Error: Can not instantiate custom module " + module.className, e); - } + this.addCustomModule(module.className, configBuilder); } else if (module.name != null) { - switch (module.name) { - case "Jackson": - this.getLog().debug("- Adding Jackson Module"); - addJacksonModule(configBuilder, module); - break; - case "JakartaValidation": - this.getLog().debug("- Adding Jakarta Validation Module"); - addJakartaValidationModule(configBuilder, module); - break; - case "JavaxValidation": - this.getLog().debug("- Adding Javax Validation Module"); - addJavaxValidationModule(configBuilder, module); - break; - case "Swagger15": - this.getLog().debug("- Adding Swagger 1.5 Module"); - addSwagger15Module(configBuilder, module); - break; - case "Swagger2": - this.getLog().debug("- Adding Swagger 2.x Module"); - addSwagger2Module(configBuilder, module); - break; - default: - throw new MojoExecutionException("Error: Module does not have a name in " - + "['Jackson', 'JakartaValidation', 'JavaxValidation', 'Swagger15', 'Swagger2'] or does not have a custom classname."); - } + this.addStandardModule(module, configBuilder); } } } /** - * Construct the classloader based on the project classpath. + * Instantiate and apply the custom module with the given class name to the config builder. * - * @return The classloader + * @param moduleClassName Class name of the custom module to add. + * @param configBuilder The builder on which the module is added. + * @throws MojoExecutionException When failing to instantiate the indicated module class. */ - private URLClassLoader getClassLoader() { - if (this.classLoader == null) { - // fix the classpath such that the classloader can get classes from any possible dependency - // this does not affect filtering, as the classgraph library uses its own classloader and allows for caching - List urls = ClasspathType.WITH_ALL_DEPENDENCIES_AND_TESTS.getUrls(this.project); - this.classLoader = new URLClassLoader(urls.toArray(new URL[0]), - Thread.currentThread().getContextClassLoader()); - } - return this.classLoader; - } - - /** - * Load a class from the plugin classpath enriched with the project dependencies. - * - * @param className Name of the class to be loaded - * @return The loaded class - * @throws MojoExecutionException In case of unexpected behavior - */ - private Class loadClass(String className) throws MojoExecutionException { + private void addCustomModule(String moduleClassName, SchemaGeneratorConfigBuilder configBuilder) throws MojoExecutionException { + this.getLog().debug("- Adding custom Module " + moduleClassName); try { - return this.getClassLoader().loadClass(className); - } catch (ClassNotFoundException e) { - throw new MojoExecutionException("Error loading class " + className, e); + Class moduleClass = (Class) this.loadClass(moduleClassName); + Module moduleInstance = moduleClass.getConstructor().newInstance(); + configBuilder.with(moduleInstance); + } catch (ClassCastException | InstantiationException + | IllegalAccessException | NoSuchMethodException + | InvocationTargetException e) { + throw new MojoExecutionException("Error: Can not instantiate custom module " + moduleClassName, e); } } /** - * Add the Swagger (1.5) module to the generator config. + * Instantiate and apply the standard module with the given name to the config builder. * - * @param configBuilder The builder on which the config is added - * @param module The modules section form the pom - * @throws MojoExecutionException in case of problems - */ - private void addSwagger15Module(SchemaGeneratorConfigBuilder configBuilder, GeneratorModule module) throws MojoExecutionException { - if (module.options == null || module.options.length == 0) { - configBuilder.with(new SwaggerModule()); - } else { - SwaggerOption[] swaggerOptions = new SwaggerOption[module.options.length]; - for (int i = 0; i < module.options.length; i++) { - try { - swaggerOptions[i] = SwaggerOption.valueOf(module.options[i]); - } catch (IllegalArgumentException e) { - throw new MojoExecutionException("Error: Unknown Swagger option " + module.options[i], e); - } - } - configBuilder.with(new SwaggerModule(swaggerOptions)); + * @param module Record in the modules section from the pom containing at least a name. + * @param configBuilder The builder on which the module is added. + * @throws MojoExecutionException When an invalid module name or option is specified. + */ + private void addStandardModule(GeneratorModule module, SchemaGeneratorConfigBuilder configBuilder) throws MojoExecutionException { + switch (module.name) { + case "Jackson": + this.getLog().debug("- Adding Jackson Module"); + this.addStandardModuleWithOptions(module, configBuilder, JacksonModule::new, JacksonOption.class); + break; + case "JakartaValidation": + this.getLog().debug("- Adding Jakarta Validation Module"); + this.addStandardModuleWithOptions(module, configBuilder, JakartaValidationModule::new, JakartaValidationOption.class); + break; + case "JavaxValidation": + this.getLog().debug("- Adding Javax Validation Module"); + this.addStandardModuleWithOptions(module, configBuilder, JavaxValidationModule::new, JavaxValidationOption.class); + break; + case "Swagger15": + this.getLog().debug("- Adding Swagger 1.5 Module"); + this.addStandardModuleWithOptions(module, configBuilder, SwaggerModule::new, SwaggerOption.class); + break; + case "Swagger2": + this.getLog().debug("- Adding Swagger 2.x Module"); + configBuilder.with(new Swagger2Module()); + break; + default: + throw new MojoExecutionException("Error: Module does not have a name in " + + "['Jackson', 'JakartaValidation', 'JavaxValidation', 'Swagger15', 'Swagger2'] or does not have a custom classname."); } } /** - * Add the Swagger (2.x) module to the generator config. + * Add a standard module to the generator configuration. * - * @param configBuilder The builder on which the config is added - * @param module The modules section form the pom + * @param type of option enum the standard module expects in its constructor + * @param configBuilder builder on which the standard module should be added + * @param module record in the modules section from the pom + * @param moduleConstructor module constructor expecting an array of options + * @param optionType enum type for the module options (e.g., JacksonOption or JakartaValidationOption) * @throws MojoExecutionException in case of problems */ - private void addSwagger2Module(SchemaGeneratorConfigBuilder configBuilder, GeneratorModule module) throws MojoExecutionException { - configBuilder.with(new Swagger2Module()); - } - - /** - * Add the Javax Validation module to the generator config. - * - * @param configBuilder The builder on which the config is added - * @param module The modules section form the pom - * @throws MojoExecutionException in case of problems - */ - private void addJavaxValidationModule(SchemaGeneratorConfigBuilder configBuilder, GeneratorModule module) throws MojoExecutionException { - if (module.options == null || module.options.length == 0) { - configBuilder.with(new JavaxValidationModule()); - } else { - JavaxValidationOption[] javaxValidationOptions = new JavaxValidationOption[module.options.length]; - for (int i = 0; i < module.options.length; i++) { + 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 { - javaxValidationOptions[i] = JavaxValidationOption.valueOf(module.options[i]); + optionStream.add(Enum.valueOf(optionType, optionName)); } catch (IllegalArgumentException e) { - throw new MojoExecutionException("Error: Unknown JavaxValidation option " + module.options[i], e); + throw new MojoExecutionException("Error: Unknown " + module.name + " option " + optionName, e); } } - configBuilder.with(new JavaxValidationModule(javaxValidationOptions)); } + T[] options = optionStream.build().toArray(count -> (T[]) Array.newInstance(optionType, count)); + configBuilder.with(moduleConstructor.apply(options)); } /** - * Add the Jakarta Validation module to the generator config. + * Construct the classloader based on the project classpath. * - * @param configBuilder The builder on which the config is added - * @param module The modules section form the pom - * @throws MojoExecutionException in case of problems + * @return The classloader */ - private void addJakartaValidationModule(SchemaGeneratorConfigBuilder configBuilder, GeneratorModule module) throws MojoExecutionException { - if (module.options == null || module.options.length == 0) { - configBuilder.with(new JakartaValidationModule()); - } else { - JakartaValidationOption[] jakartaValidationOptions = new JakartaValidationOption[module.options.length]; - for (int i = 0; i < module.options.length; i++) { - try { - jakartaValidationOptions[i] = JakartaValidationOption.valueOf(module.options[i]); - } catch (IllegalArgumentException e) { - throw new MojoExecutionException("Error: Unknown JakartaValidation option " + module.options[i], e); - } - } - configBuilder.with(new JakartaValidationModule(jakartaValidationOptions)); + private URLClassLoader getClassLoader() { + if (this.classLoader == null) { + // fix the classpath such that the classloader can get classes from any possible dependency + // this does not affect filtering, as the classgraph library uses its own classloader and allows for caching + List urls = ClasspathType.WITH_ALL_DEPENDENCIES_AND_TESTS.getUrls(this.project); + this.classLoader = new URLClassLoader(urls.toArray(new URL[0]), + Thread.currentThread().getContextClassLoader()); } + return this.classLoader; } /** - * Add the Jackson module to the generator config. + * Load a class from the plugin classpath enriched with the project dependencies. * - * @param configBuilder The builder on which the config is added - * @param module The modules section form the pom - * @throws MojoExecutionException Exception in case of error + * @param className Name of the class to be loaded + * @return The loaded class + * @throws MojoExecutionException In case of unexpected behavior */ - private void addJacksonModule(SchemaGeneratorConfigBuilder configBuilder, GeneratorModule module) throws MojoExecutionException { - if (module.options == null || module.options.length == 0) { - configBuilder.with(new JacksonModule()); - } else { - JacksonOption[] jacksonOptions = new JacksonOption[module.options.length]; - for (int i = 0; i < module.options.length; i++) { - try { - jacksonOptions[i] = JacksonOption.valueOf(module.options[i]); - } catch (IllegalArgumentException e) { - throw new MojoExecutionException("Error: Unknown Jackson option " + module.options[i], e); - } - } - configBuilder.with(new JacksonModule(jacksonOptions)); + private Class loadClass(String className) throws MojoExecutionException { + try { + return this.getClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + throw new MojoExecutionException("Error loading class " + className, e); } }