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);
}
}
}