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

bt.torrent.compiler.MessagingAgentCompiler Maven / Gradle / Ivy

There is a newer version: 1.10
Show newest version
/*
 * Copyright (c) 2016—2017 Andrei Tomashpolskiy and individual contributors.
 *
 * 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 bt.torrent.compiler;

import bt.protocol.Message;
import bt.torrent.annotation.Consumes;
import bt.torrent.annotation.Produces;
import bt.torrent.messaging.MessageContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;

/**
 * Messaging agent compiler.
 *
 * @since 1.0
 */
public class MessagingAgentCompiler {

    private static final Logger LOGGER = LoggerFactory.getLogger(MessagingAgentCompiler.class);

    private static final String CONSUMERS_KEY = "consumers";
    private static final String PRODUCERS_KEY = "producers";

    private Map, Map>> compiledTypes;

    /**
     * @since 1.0
     */
    public MessagingAgentCompiler() {
        this.compiledTypes = new HashMap<>();
    }

    /**
     * Parse an arbitrary object.
     *
     * @param object Some object, that has methods, annotated with {@link Consumes} or {@link Produces}
     * @param visitor Provides callbacks for the compiler to invoke
     *                upon finding a consumer or producer method.
     * @since 1.0
     */
    public void compileAndVisit(Object object, CompilerVisitor visitor) {

        Class objectType = object.getClass();
        Map> compiledType = compiledTypes.get(objectType);
        if (compiledType == null) {
            compiledType = compileType(objectType);
            compiledTypes.put(objectType, compiledType);
        }

        compiledType.get(CONSUMERS_KEY).forEach(o -> {
            ConsumerInfo consumerInfo = (ConsumerInfo) o;
            visitor.visitConsumer(consumerInfo.getConsumedMessageType(), consumerInfo.getHandle());
        });

        compiledType.get(PRODUCERS_KEY).forEach(o -> {
            ProducerInfo producerInfo = (ProducerInfo) o;
            visitor.visitProducer(producerInfo.getHandle());
        });
    }

    private Map> compileType(Class type) {

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Compiling messaging agent type: " + type.getName());
        }

        Collection compiledConsumers = new ArrayList<>();
        Collection compiledProducers = new ArrayList<>();

        Map> compiledType = new HashMap>() {{
            put(CONSUMERS_KEY, compiledConsumers);
            put(PRODUCERS_KEY, compiledProducers);
        }};

        int methodCount = compileType(type, compiledConsumers, compiledProducers);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Compiled " + methodCount + " consumer/producer methods");
        }
        return compiledType;
    }

    /**
     * @return Total number of consumer/producer methods compiled
     */
    private int compileType(Class type, Collection consumersAcc, Collection producerAcc) {

        int methodCount = 0;

        for (Method method : type.getDeclaredMethods()) {

            Consumes consumes = method.getAnnotation(Consumes.class);
            Produces produces = method.getAnnotation(Produces.class);

            if (consumes != null || produces != null) {

                if (!Modifier.isPublic(method.getModifiers())) {
                    throw new IllegalStateException("Method representing consumer/producer must be public: " + method.getName());
                }
                if (consumes != null && produces != null) {
                    throw new IllegalStateException("Method can not be both consumer and producer: " + method.getName());
                }

                if (consumes != null) {
                    ConsumerInfo consumerInfo = buildConsumerInfo(method);
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug("Compiled consumer method {consumedType=" +
                                consumerInfo.getConsumedMessageType().getName() +
                                "}: " + method.getName());
                    }
                    consumersAcc.add(consumerInfo);
                } else if (produces != null) {
                    ProducerInfo producerInfo = buildProducerInfo(method);
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug("Compiled producer method: " + method.getName());
                    }
                    producerAcc.add(producerInfo);
                }

                methodCount++;
            }
        }

        Class supertype = type.getSuperclass();
        if (supertype != null && !Object.class.equals(supertype)) {
            methodCount += compileType(supertype, consumersAcc, producerAcc);
        }

        Class[] interfaceTypes = type.getInterfaces();
        for (Class interfaceType : interfaceTypes) {
            methodCount += compileType(interfaceType, consumersAcc, producerAcc);
        }

        return methodCount;
    }

    private static ConsumerInfo buildConsumerInfo(Method method) {

        Class[] parameterTypes = method.getParameterTypes();

        if (parameterTypes.length == 0 || parameterTypes.length > 2) {
            throw new IllegalStateException("Consumer method must have at least one and at most two parameters: " +
                Message.class.getName() + " or it's subclass (required), " +
                    MessageContext.class.getName() + " (optional)");
        }
        if (!Message.class.isAssignableFrom(parameterTypes[0])) {
            throw new IllegalStateException("Consumer method must have " + Message.class.getName() +
                    " or it's subclass as the first parameter");
        }
        if (parameterTypes.length == 2) {
            if (!MessageContext.class.equals(parameterTypes[1])) {
                throw new IllegalStateException("Consumer method must have " + MessageContext.class.getName() +
                        " as the second parameter");
            }
        }

        @SuppressWarnings("unchecked")
        Class consumedType = (Class) parameterTypes[0];

        ConsumerInfo consumerInfo = new ConsumerInfo();
        consumerInfo.setConsumedMessageType(consumedType);

        MethodHandle handle;
        try {
            handle = MethodHandles.publicLookup().unreflect(method);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Failed to create method handle: " + method.getName(), e);
        }
        consumerInfo.setHandle(handle);

        return consumerInfo;
    }

    private ProducerInfo buildProducerInfo(Method method) {

        Class[] parameterTypes = method.getParameterTypes();

        if (parameterTypes.length == 0 || parameterTypes.length > 2) {
            throw new IllegalStateException("Producer method must have at least one and at most two parameters: " +
                Consumer.class.getName() + "<" + Message.class.getName() +  "> (required), " +
                    MessageContext.class.getName() + " (optional)");
        }
        Optional argumentType = unwrapSingleTypeParameter(method.getGenericParameterTypes()[0]);
        if (!Consumer.class.isAssignableFrom(parameterTypes[0])
                || !argumentType.isPresent() || !Message.class.equals(argumentType.get())) {
            throw new IllegalStateException("Producer method must have " + Consumer.class.getName() +
                    "<" + Message.class.getName() + "> as the first parameter");
        }
        if (parameterTypes.length == 2) {
            if (!MessageContext.class.equals(parameterTypes[1])) {
                throw new IllegalStateException("Producer method must have " + MessageContext.class.getName() +
                        " as the second parameter");
            }
        }

        MethodHandle handle;
        try {
            handle = MethodHandles.publicLookup().unreflect(method);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Failed to create method handle: " + method.getName(), e);
        }

        ProducerInfo producerInfo = new ProducerInfo();
        producerInfo.setHandle(handle);
        return producerInfo;
    }

    private static Optional unwrapSingleTypeParameter(Type type) {
        if (type instanceof ParameterizedType) {
            Type argumentType = ((ParameterizedType) type).getActualTypeArguments()[0];
            return Optional.of(argumentType);
        }
        return Optional.empty();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy