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

org.iris_events.asyncapi.runtime.scanner.IrisAnnotationScanner Maven / Gradle / Ivy

/*
 * Copyright 2019 Red Hat
 *
 * 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 org.iris_events.asyncapi.runtime.scanner;

import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.iris_events.annotations.CachedMessage;
import org.iris_events.annotations.Message;
import org.iris_events.annotations.MessageHandler;
import org.iris_events.annotations.Scope;
import org.iris_events.annotations.SnapshotMessageHandler;
import org.iris_events.asyncapi.api.AsyncApiConfig;
import org.iris_events.asyncapi.parsers.BindingKeysParser;
import org.iris_events.asyncapi.parsers.CacheableTtlParser;
import org.iris_events.asyncapi.parsers.DeadLetterQueueParser;
import org.iris_events.asyncapi.parsers.ExchangeParser;
import org.iris_events.asyncapi.parsers.ExchangeTtlParser;
import org.iris_events.asyncapi.parsers.ExchangeTypeParser;
import org.iris_events.asyncapi.parsers.MessageScopeParser;
import org.iris_events.asyncapi.parsers.PersistentParser;
import org.iris_events.asyncapi.parsers.QueueAutoDeleteParser;
import org.iris_events.asyncapi.parsers.QueueDurableParser;
import org.iris_events.asyncapi.parsers.ResourceTypeParser;
import org.iris_events.asyncapi.parsers.ResponseParser;
import org.iris_events.asyncapi.parsers.RolesAllowedParser;
import org.iris_events.asyncapi.parsers.RoutingKeyParser;
import org.iris_events.asyncapi.parsers.RpcResponseClassParser;
import org.iris_events.asyncapi.runtime.generator.CustomDefinitionProvider;
import org.iris_events.asyncapi.runtime.io.components.ComponentReader;
import org.iris_events.asyncapi.runtime.scanner.model.ChannelInfo;
import org.iris_events.asyncapi.runtime.scanner.model.GidOpenApiModule;
import org.iris_events.asyncapi.runtime.scanner.model.GidOpenApiOption;
import org.iris_events.asyncapi.runtime.scanner.model.JsonSchemaInfo;
import org.iris_events.asyncapi.runtime.scanner.validator.MessageAnnotationValidator;
import org.iris_events.asyncapi.runtime.util.ChannelInfoGenerator;
import org.iris_events.asyncapi.runtime.util.SchemeIdGenerator;
import org.iris_events.common.HandlerDefaultParameter;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
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.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaVersion;
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;
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationOption;

import io.apicurio.datamodels.models.asyncapi.v26.AsyncApi26Document;
import io.apicurio.datamodels.models.asyncapi.v26.AsyncApi26InfoImpl;

/**
 * Scans a deployment (using the archive and jandex annotation index) for relevant annotations. These
 * annotations, if found, are used to generate a valid AsyncAPI model.
 *
 * @author [email protected]
 */
public class IrisAnnotationScanner extends BaseAnnotationScanner {
    private static final Logger LOG = LoggerFactory.getLogger(IrisAnnotationScanner.class);

    private final SchemaGenerator schemaGenerator;
    private final String projectName;
    private final String projectGroupId;
    private final String projectVersion;

    public static final DotName DOT_NAME_MESSAGE = DotName.createSimple(Message.class.getName());
    public static final DotName DOT_NAME_CACHED_MESSAGE = DotName.createSimple(CachedMessage.class.getName());
    public static final DotName DOT_NAME_MESSAGE_HANDLER = DotName.createSimple(MessageHandler.class.getName());
    public static final DotName DOT_NAME_SNAPSHOT_MESSAGE_HANDLER = DotName
            .createSimple(SnapshotMessageHandler.class.getName());

    /**
     * Constructor.
     *
     * @param config AsyncApiConfig instance
     * @param index IndexView of deployment
     * @param projectName Name of project
     */
    public IrisAnnotationScanner(AsyncApiConfig config, IndexView index, String projectName, String projectGroupId,
            String projectVersion, ObjectMapper objectMapper) {
        super(config, index, objectMapper);
        this.schemaGenerator = initSchemaGenerator(config);
        this.projectName = projectName;
        this.projectGroupId = projectGroupId;
        this.projectVersion = projectVersion;
    }

    public IrisAnnotationScanner(AsyncApiConfig config, IndexView index, ClassLoader classLoader, String projectName,
            String projectGroupId, String projectVersion, ObjectMapper objectMapper) {
        super(config, index, classLoader, objectMapper);
        this.schemaGenerator = initSchemaGenerator(config);
        this.projectName = projectName;
        this.projectGroupId = projectGroupId;
        this.projectVersion = projectVersion;
    }

    /**
     * Scan the deployment for relevant annotations. Returns an AsyncAPI data model that was
     * built from those found annotations.
     *
     * @return Document generated from scanning annotations
     */
    public AsyncApi26Document scan() {
        LOG.debug("Scanning deployment for Async Annotations.");
        try {
            return scanIrisAnnotations();
        } catch (ClassNotFoundException e) {
            LOG.error("Could not create AaiDocument", e);
            throw new RuntimeException("Could not create AaiDocument", e);
        }
    }

    private AsyncApi26Document scanIrisAnnotations() throws ClassNotFoundException {
        final var asyncApi = this.annotationScannerContext.getAsyncApi();
        setDocumentInfo(asyncApi);
        // Process @Message
        final var messageAnnotations = getMessageAnnotations(this.annotationScannerContext.getIndex()).collect(
                Collectors.toList());
        LOG.debug(String.format("Got %s message annotations", messageAnnotations.size()));

        final var validator = new MessageAnnotationValidator();
        validator.validateReservedNames(messageAnnotations, this.projectName, this.projectGroupId);

        final var handlerAnnotations = getHandlerAnnotations(annotationScannerContext.getIndex(),
                List.of(DOT_NAME_MESSAGE_HANDLER, DOT_NAME_SNAPSHOT_MESSAGE_HANDLER));
        final var consumedMessageAnnotations = processMessageHandlerAnnotations(handlerAnnotations,
                annotationScannerContext, asyncApi);

        LOG.debug(String.format("Got %s consumed message annotations", consumedMessageAnnotations.size()));

        processProducedMessages(annotationScannerContext, asyncApi, messageAnnotations, consumedMessageAnnotations);
        processContextDefinitionReferencedSchemas(annotationScannerContext, asyncApi);

        return asyncApi;
    }

    private List getHandlerAnnotations(IndexView index, List handlerDotNames) {
        return handlerDotNames.stream()
                .map(index::getAnnotations)
                .flatMap(Collection::stream)
                .filter(this::annotatedMethods)
                .toList();
    }

    private Stream getMessageAnnotations(IndexView index) {
        final var annotationName = DotName.createSimple(Message.class.getName());
        return index.getAnnotations(annotationName).stream().filter(this::annotatedClasses);
    }

    private void processProducedMessages(AnnotationScannerContext context,
            AsyncApi26Document asyncApi,
            List messageAnnotations,
            List consumedMessageAnnotations)
            throws ClassNotFoundException {
        final var index = context.getIndex();
        messageAnnotations.removeAll(consumedMessageAnnotations);

        final var channelInfos = new ArrayList();
        final var producedMessages = new HashMap();
        final var messageScopes = new HashMap();
        for (AnnotationInstance anno : messageAnnotations) {
            final var classInfo = anno.target().asClass();
            final var classSimpleName = classInfo.simpleName();

            messageScopes.put(classSimpleName, MessageScopeParser.getFromAnnotationInstance(anno, index));
            producedMessages.put(classSimpleName, generateProducedMessageSchemaInfo(classInfo, index));

            final var routingKey = RoutingKeyParser.getFromAnnotationInstance(anno);
            final var exchangeType = ExchangeTypeParser.getFromAnnotationInstance(anno, index);
            final var exchange = ExchangeParser.getFromAnnotationInstance(anno);

            // Header values
            final var rolesAllowed = RolesAllowedParser.getFromAnnotationInstance(anno, index);
            final var deadLetterQueue = DeadLetterQueueParser.getFromAnnotationInstance(anno, index);
            final var ttl = ExchangeTtlParser.getFromAnnotationInstance(anno, index);
            final var persistent = PersistentParser.getFromAnnotationInstance(anno, index);

            channelInfos.add(ChannelInfoGenerator.generateSubscribeChannelInfo(
                    exchange,
                    routingKey,
                    classSimpleName,
                    exchangeType,
                    rolesAllowed,
                    deadLetterQueue,
                    ttl,
                    persistent));
        }

        insertComponentSchemas(context, producedMessages, asyncApi);

        // TODO check what's with the types

        createChannels(channelInfos, messageScopes, asyncApi);
    }

    private List processMessageHandlerAnnotations(List methodAnnotationInstances,
            AnnotationScannerContext context, AsyncApi26Document asyncApi)
            throws ClassNotFoundException {

        final var consumedMessages = new ArrayList();
        final var index = context.getIndex();
        final var incomingMessages = new HashMap();
        final var channelInfos = new ArrayList();
        final var messageTypes = new HashMap();

        for (AnnotationInstance handlerAnnotation : methodAnnotationInstances) {

            final var annotationName = handlerAnnotation.name();
            final var annotationValues = handlerAnnotation.values();
            final var methodInfo = (MethodInfo) handlerAnnotation.target();
            final var methodParameters = methodInfo.parameterTypes();

            final var messageAnnotation = getMessageAnnotation(methodParameters, index);
            consumedMessages.add(messageAnnotation);

            final var messageClass = messageAnnotation.target().asClass();
            final var messageClassSimpleName = messageClass.simpleName();

            final var bindingKeys = getBindingKeys(handlerAnnotation, messageAnnotation);
            final var exchangeType = ExchangeTypeParser.getFromAnnotationInstance(messageAnnotation, index);
            final var exchange = ExchangeParser.getFromAnnotationInstance(messageAnnotation);
            final var scope = MessageScopeParser.getFromAnnotationInstance(messageAnnotation, index);

            final var durable = getDurable(handlerAnnotation, index);
            final var autoDelete = getAutoDelete(handlerAnnotation, index);
            final var deadLetterQueue = DeadLetterQueueParser.getFromAnnotationInstance(messageAnnotation, index);
            final var ttl = ExchangeTtlParser.getFromAnnotationInstance(messageAnnotation, index);
            final var persistent = PersistentParser.getFromAnnotationInstance(messageAnnotation, index);
            final var responseType = ResponseParser.getFromAnnotationInstance(messageAnnotation, index);

            final var rpcResponseType = RpcResponseClassParser.getFromAnnotationInstance(messageAnnotation, index);

            final var isGeneratedClass = isGeneratedClass(messageClass);

            final var jsonSchemaInfo = generateConsumedMessageJsonSchemaInfo(
                    annotationName,
                    messageClass.name().toString(),
                    annotationValues,
                    isGeneratedClass);

            final var subscribeChannelInfo = ChannelInfoGenerator.generatePublishChannelInfo(
                    exchange,
                    bindingKeys,
                    messageClassSimpleName,
                    exchangeType,
                    durable,
                    autoDelete,
                    RolesAllowedParser.getFromAnnotationInstance(handlerAnnotation, index),
                    deadLetterQueue,
                    ttl,
                    responseType,
                    persistent,
                    rpcResponseType);

            messageTypes.put(messageClassSimpleName, scope);
            incomingMessages.put(messageClassSimpleName, jsonSchemaInfo);
            channelInfos.add(subscribeChannelInfo);
        }

        insertComponentSchemas(context, incomingMessages, asyncApi);
        createChannels(channelInfos, messageTypes, asyncApi);

        return consumedMessages;
    }

    private String getBindingKeys(AnnotationInstance handlerAnnotationInstance, AnnotationInstance messageAnnotation) {
        final var dotName = handlerAnnotationInstance.name();
        if (DOT_NAME_SNAPSHOT_MESSAGE_HANDLER.equals(dotName)) {
            return ResourceTypeParser.getFromAnnotationInstance(handlerAnnotationInstance);
        } else if (DOT_NAME_MESSAGE_HANDLER.equals(dotName)) {
            return BindingKeysParser.getFromAnnotationInstanceAsCsv(handlerAnnotationInstance, messageAnnotation);
        }
        throw new IllegalArgumentException("Unsupported annotation instance " + dotName);
    }

    private boolean getDurable(AnnotationInstance handlerAnnotationInstance, FilteredIndexView indexView) {
        final var dotName = handlerAnnotationInstance.name();
        if (DOT_NAME_SNAPSHOT_MESSAGE_HANDLER.equals(dotName)) {
            return HandlerDefaultParameter.SnapshotMessageHandler.DURABLE;
        } else if (DOT_NAME_MESSAGE_HANDLER.equals(dotName)) {
            return QueueDurableParser.getFromAnnotationInstance(handlerAnnotationInstance, indexView);
        }
        throw new IllegalArgumentException("Unsupported annotation instance " + dotName);
    }

    private boolean getAutoDelete(AnnotationInstance handlerAnnotationInstance, FilteredIndexView indexView) {
        final var dotName = handlerAnnotationInstance.name();
        if (DOT_NAME_SNAPSHOT_MESSAGE_HANDLER.equals(dotName)) {
            return HandlerDefaultParameter.SnapshotMessageHandler.AUTO_DELETE;
        } else if (DOT_NAME_MESSAGE_HANDLER.equals(dotName)) {
            return QueueAutoDeleteParser.getFromAnnotationInstance(handlerAnnotationInstance, indexView);
        }
        throw new IllegalArgumentException("Unsupported annotation instance " + dotName);
    }

    private void processContextDefinitionReferencedSchemas(AnnotationScannerContext context, AsyncApi26Document asyncApi) {
        final var definitionSchemaMap = context.getDefinitionSchemaMap();
        asyncApi.getComponents().getSchemas().putAll(definitionSchemaMap);
        context.clearDefinitionSchemaMap();
    }

    private AsyncApi26Document setDocumentInfo(AsyncApi26Document document) {
        try {
            final var projectSchemaId = SchemeIdGenerator.buildId(projectName);

            final var info = new AsyncApi26InfoImpl();
            info.setTitle(projectName);
            info.setVersion(this.projectVersion);

            document.setId(projectSchemaId);
            document.setInfo(info);
            document.setComponents(ComponentReader.create());
            return document;
        } catch (URISyntaxException e) {
            LOG.error("Could not generate schema ID", e);
            throw new RuntimeException(e);
        }
    }

    private JsonSchemaInfo generateProducedMessageSchemaInfo(ClassInfo classInfo, final FilteredIndexView index)
            throws ClassNotFoundException {
        final var className = classInfo.name().toString();
        final var loadedClass = loadClass(className);
        final var classSimpleName = loadedClass.getSimpleName();
        final var isGeneratedClass = isGeneratedClass(classInfo);
        final var cacheTtl = getCacheTtl(classInfo, index);
        final var generatedSchema = schemaGenerator.generateSchema(loadedClass);
        fixDocumentedRefProperties(className, generatedSchema);

        return new JsonSchemaInfo(
                null,
                classSimpleName,
                generatedSchema,
                null,
                isGeneratedClass,
                cacheTtl);
    }

    private JsonSchemaInfo generateConsumedMessageJsonSchemaInfo(DotName annotationName, String className,
            List annotationValues, boolean isGeneratedClass) throws ClassNotFoundException {
        final var loadedClass = loadClass(className);
        final var eventSimpleName = loadedClass.getSimpleName();
        final var generatedSchema = schemaGenerator.generateSchema(loadedClass);
        fixDocumentedRefProperties(className, generatedSchema);

        return new JsonSchemaInfo(
                annotationName,
                eventSimpleName,
                generatedSchema,
                annotationValues,
                isGeneratedClass,
                null);
    }

    private static void fixDocumentedRefProperties(final String className, final ObjectNode generatedSchema) {
        final var properties = generatedSchema.get("properties");
        if (properties != null) {
            StreamSupport.stream(properties.spliterator(), false)
                    .filter(jsonNode -> jsonNode.has("allOf"))
                    .forEach(jsonNode -> {
                        final var objectNode = (ObjectNode) jsonNode;
                        var allOf = jsonNode.get("allOf");
                        allOf.forEach(allOfItem -> allOfItem.fields().forEachRemaining(stringJsonNodeEntry -> objectNode
                                .set(stringJsonNodeEntry.getKey(), stringJsonNodeEntry.getValue())));
                        objectNode.remove("allOf");
                    });
        }
    }

    private Class loadClass(String className) throws ClassNotFoundException {
        if (classLoader != null) {
            return classLoader.loadClass(className);
        } else {
            return Class.forName(className);
        }
    }

    private SchemaGenerator initSchemaGenerator(AsyncApiConfig config) {
        final var jacksonModule = new JacksonModule(JacksonOption.FLATTENED_ENUMS_FROM_JSONPROPERTY);
        final var gidOpenApiModule = new GidOpenApiModule(
                GidOpenApiOption.IGNORING_HIDDEN_PROPERTIES,
                GidOpenApiOption.ENABLE_PROPERTY_NAME_OVERRIDES);
        final var jakartaValidationModule = new JakartaValidationModule(
                JakartaValidationOption.NOT_NULLABLE_FIELD_IS_REQUIRED,
                JakartaValidationOption.NOT_NULLABLE_METHOD_IS_REQUIRED,
                JakartaValidationOption.PREFER_IDN_EMAIL_FORMAT,
                JakartaValidationOption.INCLUDE_PATTERN_EXPRESSIONS);
        final var configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_7,
                OptionPreset.PLAIN_JSON)
                .with(Option.DEFINITIONS_FOR_ALL_OBJECTS)
                .with(jacksonModule)
                .with(jakartaValidationModule)
                .with(gidOpenApiModule);

        configBuilder.forTypesInGeneral()
                .withCustomDefinitionProvider(
                        CustomDefinitionProvider.convertTypesToObject(config.excludeFromSchemas()));

        configBuilder.forFields()
                .withCustomDefinitionProvider(
                        CustomDefinitionProvider.convertFieldsToObject(config.excludeFromSchemas()));

        configBuilder.with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES);
        return new SchemaGenerator(configBuilder.build());
    }

    private AnnotationInstance getMessageAnnotation(final List parameters,
            final FilteredIndexView index) {

        final var consumedEventTypes = parameters.stream()
                .map(Type::name)
                .map(index::getClassByName)
                .filter(Objects::nonNull)
                .map(classInfo -> classInfo.declaredAnnotation(DOT_NAME_MESSAGE))
                .filter(Objects::nonNull).toList();

        if (consumedEventTypes.isEmpty()) {
            throw new IllegalArgumentException(String.format("Consumed Event not found for parameters %s", parameters));
        }

        if (consumedEventTypes.size() > 1) {
            throw new IllegalArgumentException(
                    "Multiple consumed Events detected. Message handler can only handle one event type.");
        }

        return consumedEventTypes.get(0);
    }

    private Integer getCacheTtl(final ClassInfo classInfo, final FilteredIndexView index) {
        final var annotationInstance = classInfo.declaredAnnotation(DOT_NAME_CACHED_MESSAGE);
        if (annotationInstance == null) {
            return null;
        }

        return CacheableTtlParser.getFromAnnotationInstance(annotationInstance, index);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy