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

net.openhft.chronicle.wire.GenerateJsonSchemaMain Maven / Gradle / Ivy

There is a newer version: 2.27ea1
Show newest version
/*
 * Copyright 2016-2022 chronicle.software
 *
 *       https://chronicle.software
 *
 * 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 net.openhft.chronicle.wire;

import net.openhft.chronicle.core.Jvm;
import net.openhft.chronicle.wire.utils.JsonSourceCodeFormatter;
import net.openhft.chronicle.wire.utils.SourceCodeFormatter;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * This is the GenerateJsonSchemaMain class.
 * Its primary function is to generate a JSON schema based on the provided set of classes.
 * The generated JSON schema represents the structure and types of the specified classes.
 */
public class GenerateJsonSchemaMain {

    // A mapping of Java Classes to their corresponding JSON data types.
    final Map, String> aliases = new LinkedHashMap<>();

    // A mapping of Java Classes to their corresponding JSON schema definitions.
    final Map, String> definitions = new LinkedHashMap<>();

    // A sorted mapping of event names to their JSON schema representations.
    final Map events = new TreeMap<>();

    // A set containing classes that represent events.
    final Set> eventClasses = new LinkedHashSet<>();

    /**
     * Default constructor initializing some common aliases for Java types to JSON types.
     */
    public GenerateJsonSchemaMain() {
        aliases.put(void.class, "null");
        aliases.put(Void.class, "null");
        aliases.put(String.class, "string");
        aliases.put(byte.class, "integer");
        aliases.put(short.class, "integer");
        aliases.put(int.class, "integer");
        aliases.put(long.class, "integer");
        aliases.put(float.class, "number");
        aliases.put(double.class, "number");
        aliases.put(boolean.class, "boolean");
    }

    /**
     * Main entry point of the application.
     * For each provided class name argument, it generates a corresponding JSON schema and prints it.
     *
     * @param args Array of class names to generate JSON schema for.
     * @throws ClassNotFoundException if any of the provided class names is not found.
     */
    public static void main(String... args) throws ClassNotFoundException {
        final String json = main0(args);
        System.out.println(json);
    }

    /**
     * Processes the provided class names, generates the JSON schema for each, and returns the combined schema.
     *
     * @param args Array of class names to generate JSON schema for.
     * @return The combined JSON schema for all provided class names.
     * @throws ClassNotFoundException if any of the provided class names is not found.
     */
    static String main0(String... args) throws ClassNotFoundException {
        Set> interfaces = new LinkedHashSet<>();
        for (String arg : args) {
            interfaces.add(Class.forName(arg));
        }
        GenerateJsonSchemaMain g = new GenerateJsonSchemaMain();
        for (Class aClass : interfaces) {
            g.generateEventSchemaFor(aClass);
        }
        final String json = g.asJson();
        return json;
    }

    /**
     * Generates and returns a JSON-formatted schema string.
     * The schema is constructed based on the definitions and events stored in the instance.
     *
     * @return A string representation of the JSON schema.
     */
    String asJson() {
        SourceCodeFormatter sb = new JsonSourceCodeFormatter();
        String str = "{\n" +
                "\"$schema\": \"http://json-schema.org/draft-07/schema#\",\n" +
                "\"$id\": \"http://json-schema.org/draft-07/schema#\",\n" +
                "\"title\": \"Core schema meta-schema\",\n" +
                "\"definitions\": {\n";
        sb.append(str);
        String sep = "";
        for (Map.Entry, String> entry : definitions.entrySet()) {
            sb.append(sep);
            sb.append("\"" + entry.getKey().getSimpleName() + "\": {\n");
            sb.append(entry.getValue());
            sb.append("}");
            sep = ",\n";
        }
        sb.append("\n");
        sb.append("},\n" +
                "\"properties\": {\n");
        for (Map.Entry entry : events.entrySet()) {
            sb.append("\"" + entry.getKey() + "\": {\n");
            sb.append(entry.getValue());
            sb.append("},\n");
        }
        sb.append("}\n" +
                "}\n");
        return sb.toString();
    }

    /**
     * Generates a JSON schema representation for events based on the given type.
     * It processes the type's methods to derive the schema.
     * The generated schema is stored within the 'events' map, with the method name as the key
     * and the generated schema description as the value.
     *
     * @param type The class type for which the event schema is to be generated.
     */
    void generateEventSchemaFor(Class type) {
        if (type.isArray())
            return;
        if (!eventClasses.add(type))
            return;
        for (Method method : type.getMethods()) {
            generateEventSchemaFor(method.getReturnType());
            Stream.of(method.getParameterTypes())
                    .forEach(this::generateObjectSchemaFor);
            StringBuilder desc = new StringBuilder();
            Class[] pTypes = method.getParameterTypes();
            Annotation[][] pAnnotations = method.getParameterAnnotations();
            switch (pTypes.length) {
                case 0:
                    desc.append("\"type\": \"constant\",\n" +
                            "\"value\": \"\""
                    );
                    break;
                case 1:
                    generateMethodDesc(desc, pTypes[0], pAnnotations[0]);
                    break;
                default:
                    Jvm.debug().on(getClass(), "Method ignored as more than 1 argument " + method);
                    break;
            }

            events.put(method.getName(), desc.toString());
        }
    }

    /**
     * Constructs and appends properties in JSON schema format to the provided StringBuilder.
     * The provided properties map holds the property names and their corresponding JSON definitions.
     * The resulting JSON schema structure will encapsulate these properties within a 'properties' object.
     *
     * @param properties A map of property names to their JSON schema representations.
     * @param sb         The StringBuilder to which the properties will be appended in JSON schema format.
     */
    private void addProperties(Map properties, StringBuilder sb) {
        sb.append("\"properties\": {");
        String sep = "\n";
        for (Map.Entry entry : properties.entrySet()) {
            sb.append(sep);
            sb.append("\"" + entry.getKey() + "\": {\n");
            sb.append(entry.getValue());
            sb.append("}");
            sep = ",\n";
        }
        sb.append("\n" +
                "}\n");
    }

    /**
     * Generates a JSON schema representation for objects based on the given type.
     * This method processes the type's fields to derive the schema.
     * The generated schema is stored within the 'definitions' map with the class name as the key
     * and the generated schema description as the value.
     * If the type is already present in the 'aliases' map, the method will return without generating the schema.
     *
     * @param type The class type for which the object schema is to be generated.
     */
    void generateObjectSchemaFor(Class type) {
        if (type.isArray())
            return;
        if (aliases.containsKey(type))
            return;
        aliases.put(type, "#/definitions/" + type.getSimpleName());
        Set required = new LinkedHashSet<>();
        Map properties = new LinkedHashMap<>();
        StringBuilder sb = new StringBuilder();
        Map fieldMap = new LinkedHashMap<>();
        WireMarshaller.getAllField(type, fieldMap);
        for (Map.Entry entry : fieldMap.entrySet()) {
            String name = entry.getKey();
            StringBuilder desc = new StringBuilder();
            Field field = entry.getValue();
            Class fType = field.getType();
            Annotation[] annotations = field.getAnnotations();
            addTypeForFieldOrParam(desc, fType, annotations);
            if (fType.isPrimitive() || hasNotNull(annotations))
                required.add(name);
            properties.put(name, desc.toString());
        }
        sb.append("\"type\": \"object\",\n");
        if (!required.isEmpty()) {
            sb.append("\"required\": [\n");
            sb.append(required.stream()
                    .map(s -> '"' + s + '"')
                    .collect(Collectors.joining(",\n")));
            sb.append("\n" +
                    "],\n");
        }
        Comment comment = Jvm.findAnnotation(type, Comment.class);
        if (comment != null)
            sb.append("\"description\": \"" + comment.value() + "\",\n");

        addProperties(properties, sb);
        definitions.put(type, sb.toString());
    }

    /**
     * Determines whether the provided array of annotations contains any annotation whose name ends with '.NotNull'.
     * This method is typically used to check if a field has a NotNull validation constraint.
     *
     * @param annotations The array of annotations to be checked.
     * @return true if an annotation with name ending in '.NotNull' is found, false otherwise.
     */
    private boolean hasNotNull(Annotation[] annotations) {
        for (Annotation annotation : annotations) {
            if (annotation.annotationType().getName().endsWith(".NotNull"))
                return true;
        }
        return false;
    }

    /**
     * Generates a description for a method parameter for use in a JSON schema.
     * The description is based on the type and annotations associated with the parameter.
     *
     * @param desc The StringBuilder to which the description is appended.
     * @param pType The type of the method parameter.
     * @param annotations The array of annotations associated with the method parameter.
     */
    private void generateMethodDesc(StringBuilder desc, Class pType, Annotation[] annotations) {
        addTypeForFieldOrParam(desc, pType, annotations);
    }

    /**
     * Adds a type descriptor to the given StringBuilder based on the type and annotations
     * of a field or method parameter. This method checks for specific annotations like
     * {@link LongConversion} to determine the type description.
     * If the field/parameter is of Collection type, it's identified as an "array", and if it's
     * of type Map, it's identified as an "object". For other types, the corresponding schema is generated.
     *
     * @param desc The StringBuilder to which the type descriptor is appended.
     * @param pType The type of the field or method parameter.
     * @param annotations The array of annotations associated with the field or method parameter.
     */
    private void addTypeForFieldOrParam(StringBuilder desc, Class pType, Annotation[] annotations) {
        LongConversion lc = find(annotations, LongConversion.class);
        if (lc != null) {
            Class value = lc.value();
            if (value.getName().contains("Timestamp"))
                desc.append("\"type\": \"string\",\n" +
                        "\"format\": \"date-time\"");
            else
                desc.append("\"type\": \"string\"\n");
        } else if (Collection.class.isAssignableFrom(pType)) {
            desc.append("\"type\": \"array\"\n");
        } else if (Map.class.isAssignableFrom(pType)) {
            desc.append("\"type\": \"object\"\n");
        } else {
            generateObjectSchemaFor(pType);
            String alias = aliases.get(pType);
            String key = alias.startsWith("#") ? "$ref" : "type";
            desc.append("\"" + key + "\": \"" + alias + "\"\n");
        }
    }

    /**
     * Searches the provided array of annotations for an annotation of the specified class type.
     *
     * @param  The generic type parameter for the annotation.
     * @param annotations The array of annotations to search.
     * @param aClass The class type of the annotation to find.
     * @return The first found annotation of the specified class type, or null if not found.
     */
    private  T find(Annotation[] annotations, Class aClass) {
        for (Annotation annotation : annotations) {
            if (aClass.isAssignableFrom(annotation.annotationType()))
                return Jvm.uncheckedCast(annotation);
        }
        return null;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy