All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.softwareforge.jsonschema.JsonSchemaGenerator Maven / Gradle / Ivy

The newest version!
/*
 * 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 de.softwareforge.jsonschema;

import static com.google.common.base.Preconditions.checkState;
import static java.lang.String.format;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.reflect.TypeToken;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;

public final class JsonSchemaGenerator {

    private final JsonNodeFactory nodeFactory;
    private final JsonSchemaGeneratorConfiguration config;

    private final Set dictionary = new HashSet<>();

    JsonSchemaGenerator(JsonSchemaGeneratorConfiguration config) {
        this.nodeFactory = config.nodeFactory();
        this.config = config;
    }

    private static Optional acceptMethod(Method method) {
        // ignore weird stuff
        int modifiers = method.getModifiers();
        if (method.isBridge()
                || method.isSynthetic()
                || method.isDefault()
                || Modifier.isStatic(modifiers)) {
            return Optional.empty();
        }

        // fetch annotations for method
        return AttributeHolder.locate(method);
    }

    private static Optional acceptField(Field field) {
        // ignore weird stuff
        int modifiers = field.getModifiers();
        if (field.isEnumConstant()
                || field.isSynthetic()
                || Modifier.isTransient(modifiers)
                || Modifier.isStatic(modifiers)) {
            return Optional.empty();
        }

        // fetch annotations for method
        return AttributeHolder.locate(field);
    }

    public  ObjectNode generateSchema(Class type) {
        TypeToken typeToken = TypeToken.of(type);
        Optional rootAttributes = AttributeHolder.locate(typeToken.getRawType());

        ObjectNode schema = nodeFactory.objectNode();

        if (config.addSchemaVersion()) {
            schema.put("$schema", "http://json-schema.org/draft-04/schema#");
        }

        createSchemaForType(schema, type, rootAttributes);

        return schema;
    }

    private  void createSchemaForType(ObjectNode schema, Type type, Optional attributes) {
        if (dictionary.contains(type)) {
            throw new IllegalStateException("Recursion detected, not supported!");
        }

        Optional overriddenType = attributes.isPresent()
                ? attributes.get().type()
                : Optional.empty();

        // Simple types are a schema with their type.
        Optional s = SimpleTypeMappings.forClass(type);

        // If it is a simple type, then just put the type
        if (s.isPresent()) {
            // this is a cop-out, because only simple types can be overridden.
            addTypeToSchema(schema, overriddenType.orElse(s.get()));
            // if a format hint exists, add that as well
            SimpleTypeMappings.formatHint(type).ifPresent((formatHint) -> schema.put("format", formatHint));
        } else if (SimpleTypeMappings.isCollectionLike(type)) {
            augmentSchemaWithCollection(schema, type);
            // void to the null type. Does not really make sense.
        } else if (type == Void.class || type == void.class) {
            // this is a cop-out, because only simple types can be overridden.
            addTypeToSchema(schema, overriddenType.orElse("null"));
            // If it is an Enum than process like enum
        } else if (isEnum(type, attributes)) {
            augmentSchemaWithEnum((Class) type, schema);
            // what about map?
        } else {
            dictionary.add(type);
            augmentSchemaWithCustomType(schema, type, attributes);
            dictionary.remove(type);
        }
        attributes.ifPresent(schemaAttributes -> augmentAttributes(schema, type, schemaAttributes));
    }

    private  void augmentSchemaWithEnum(Class type, ObjectNode schema) {
        ArrayNode enumArray = schema.putArray("enum");
        for (T constant : type.getEnumConstants()) {
            String value = constant.toString();
            // Check if value is numeric
            try {
                // First verifies if it is an integer
                Long integer = Long.parseLong(value);
                enumArray.add(integer);
                // If not then verifies if it is an floating point number
            } catch (NumberFormatException e) {
                try {
                    BigDecimal number = new BigDecimal(value);
                    enumArray.add(number);
                    // Otherwise add as String
                } catch (NumberFormatException e1) {
                    enumArray.add(value);
                }
            }
        }
    }

    private  void augmentSchemaWithCustomType(ObjectNode schema, Type type, Optional attributeHolder) {
        addTypeToSchema(schema, "object");

        if (attributeHolder.isPresent()) {
            if (attributeHolder.get().ignoredProperties()) {
                return;
            }
        }

        if (config.processProperties()) {
            findSchemaPropertiesFromMethods(type, schema).forEach((propertyName, objectNode) -> addToProperties(schema, propertyName, objectNode));
        }

        if (config.processFields()) {
            findSchemaPropertiesFromFields(type, schema).forEach((propertyName, objectNode) -> addToProperties(schema, propertyName, objectNode));
        }
    }

    private Map findSchemaPropertiesFromMethods(Type type, ObjectNode parent) {
        Map propertyMap = config.sortSchemaProperties() ? new TreeMap<>() : new LinkedHashMap<>();

        TypeToken typeToken = TypeToken.of(type);

        for (TypeToken implementingTypeToken : typeToken.getTypes()) {
            Class clazz = implementingTypeToken.getRawType();

            Method[] methods = clazz.getDeclaredMethods();

            for (Method method : methods) {
                Optional attributeHolder = acceptMethod(method);
                if (attributeHolder.isPresent()) {
                    AttributeHolder attributes = attributeHolder.get();
                    String propertyName = attributes.named().orElseGet(() -> propertyName(method));

                    if (propertyMap.containsKey(propertyName)) {
                        throw new IllegalStateException(format(Locale.ENGLISH,
                                "Property %s defined multiple times (saw %s)", propertyName, clazz.getSimpleName()));
                    }

                    if (attributes.required()) {
                        addToRequired(parent, propertyName);
                    }

                    if (attributes.ignored()) {
                        continue;
                    }

                    TypeToken returnType = implementingTypeToken.resolveType(method.getGenericReturnType());

                    ObjectNode propertyNode = nodeFactory.objectNode();
                    createSchemaForType(propertyNode, returnType.getType(), Optional.of(attributes));
                    propertyMap.put(propertyName, propertyNode);
                }
            }
        }

        return propertyMap;
    }

    private Map findSchemaPropertiesFromFields(Type type, ObjectNode parent) {
        Map propertyMap = config.sortSchemaProperties() ? new TreeMap<>() : new LinkedHashMap<>();

        TypeToken typeToken = TypeToken.of(type);

        for (TypeToken implementingTypeToken : typeToken.getTypes()) {
            Class clazz = implementingTypeToken.getRawType();

            Field[] fields = clazz.getDeclaredFields();

            for (Field field : fields) {
                Optional attributeHolder = acceptField(field);
                if (attributeHolder.isPresent()) {
                    AttributeHolder attributes = attributeHolder.get();
                    String propertyName = attributes.named().orElse(propertyName(field));

                    if (propertyMap.containsKey(propertyName)) {
                        throw new IllegalStateException(format(Locale.ENGLISH,
                                "Property %s defined multiple times (saw %s)", propertyName, field.getName()));
                    }
                    if (attributes.required()) {
                        addToRequired(parent, propertyName);
                    }

                    if (attributes.ignored()) {
                        continue;
                    }

                    TypeToken fieldType = implementingTypeToken.resolveType(field.getGenericType());

                    ObjectNode propertyNode = nodeFactory.objectNode();
                    createSchemaForType(propertyNode, fieldType.getType(), Optional.of(attributes));
                    propertyMap.put(propertyName, propertyNode);
                }
            }
        }

        return propertyMap;
    }

    private void augmentAttributes(ObjectNode schema, Type type, AttributeHolder schemaAttributes) {
        schemaAttributes.augmentCommonAttributes(schema);
        schemaAttributes.$ref().ifPresent($ref -> schema.put("$ref", $ref));

        if (!schemaAttributes.additionalProperties()) {
            schema.put("additionalProperties", false);
        }

        // Check if the Nullable annotation is present, and if so, add 'null' to type attr
        if (schemaAttributes.nullable()) {
            if (isEnum(type, Optional.of(schemaAttributes))) {
                ((ArrayNode) schema.get("enum")).addNull();
            }
            addTypeToSchema(schema, "null");
        }
    }

    private boolean isEnum(Type type, Optional schemaAttributes) {
        // enum annotation enforces enum type
        if (schemaAttributes.isPresent() && !schemaAttributes.get().enums().isEmpty()) {
            return true;
        }

        // enum class type enforces enum type
        if ((type instanceof Class && ((Class) type).isEnum())) {
            return true;
        }

        return false;
    }

    private void augmentSchemaWithCollection(ObjectNode schema, Type type) {
        addTypeToSchema(schema, "array");

        TypeToken typeToken = TypeToken.of(type);

        if (typeToken.isArray()) {
            augmentItems(schema, typeToken.getComponentType().getType());
        } else {
            Class clazz = typeToken.getRawType();
            checkState(clazz.getTypeParameters().length > 0, "No type arguments in return type found!");

            Type itemType = typeToken.resolveType(clazz.getTypeParameters()[0]).getType();
            augmentItems(schema, itemType);
        }
    }

    public void augmentItems(ObjectNode schema, Type itemType) {
        ObjectNode itemNode = nodeFactory.objectNode();
        TypeToken typeToken = TypeToken.of(itemType);
        Optional itemAttributes = AttributeHolder.locate(typeToken.getRawType());
        createSchemaForType(itemNode, itemType, itemAttributes);
        schema.set("items", itemNode);
    }

    private void addToRequired(ObjectNode schema, String name) {
        ArrayNode requiredNode;
        if (schema.has("required")) {
            requiredNode = (ArrayNode) schema.get("required");
        } else {
            requiredNode = schema.putArray("required");
        }
        requiredNode.add(name);
    }

    private void addToProperties(ObjectNode schema, String name, ObjectNode property) {

        ObjectNode propertiesNode;
        if (schema.has("properties")) {
            propertiesNode = (ObjectNode) schema.get("properties");
        } else {
            propertiesNode = schema.putObject("properties");
        }
        propertiesNode.set(name, property);
    }

    private String propertyName(AnnotatedElement element) {
        if (element instanceof Field) {
            // Field name should be the same as the exposed property. Good luck.
            return ((Field) element).getName();
        } else if (element instanceof Method) {
            Method method = (Method) element;
            Class clazz = method.getDeclaringClass();
            try {
                BeanInfo info = Introspector.getBeanInfo(clazz);
                PropertyDescriptor[] props = info.getPropertyDescriptors();
                for (PropertyDescriptor pd : props) {
                    if (method.equals(pd.getWriteMethod()) || method.equals(pd.getReadMethod())) {
                        return pd.getName();
                    }
                }
            } catch (IntrospectionException e) {
                throw new IllegalStateException(format(Locale.ENGLISH, "Could not locate property name for %s", method.getName()), e);
            }

            // getter style
            if (method.getParameterCount() == 0 && method.getReturnType() != Void.TYPE) {
                return method.getName();
            }

            throw new IllegalStateException(format(Locale.ENGLISH, "Could not locate property name for %s", method.getName()));
        }
        throw new IllegalArgumentException(format(Locale.ENGLISH, "%s is not a field or method", element));
    }

    private void addTypeToSchema(ObjectNode schema, String type) {
        if (schema.has("type")) {
            JsonNode typeNode = schema.get("type");
            if (typeNode.isArray()) {
                ArrayNode arrayNode = (ArrayNode) typeNode;
                // questionable as this may create duplicates.
                arrayNode.add(type);
            } else if (typeNode.isTextual()) {
                ArrayNode typeArray = schema.putArray("type");
                typeArray.add(typeNode);
                typeArray.add(type);
                schema.replace("type", typeArray);
            } else {
                throw new IllegalStateException("Return type is not nullable");
            }
        } else {
            schema.put("type", type);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy