com.ekuefler.supereventbus.rebind.EventRegistrationWriter.java Source code

Java tutorial

Introduction

Here is the source code for com.ekuefler.supereventbus.rebind.EventRegistrationWriter.java

Source

/*
 * Copyright 2013 Erik Kuefler
 *
 * 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.ekuefler.supereventbus.rebind;

import com.ekuefler.supereventbus.Subscribe;
import com.ekuefler.supereventbus.filtering.When;
import com.ekuefler.supereventbus.multievent.EventTypes;
import com.ekuefler.supereventbus.multievent.MultiEvent;
import com.ekuefler.supereventbus.priority.WithPriority;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.user.rebind.SourceWriter;

import java.lang.reflect.Constructor;
import java.util.LinkedList;
import java.util.List;

/**
 * Writes implementations of {@link com.ekuefler.supereventbus.EventRegistration}. The generated
 * class implements {@link com.ekuefler.supereventbus.EventRegistration#getMethods} by iterating
 * over the target class's methods and generating an anonymous handler class for each method
 * annotated with {@link Subscribe}.
 *
 * @author ekuefler@gmail.com (Erik Kuefler)
 */
class EventRegistrationWriter {

    private final TreeLogger logger;

    EventRegistrationWriter(TreeLogger logger) {
        this.logger = logger;
    }

    /**
     * Writes the source for getMethods() the given target class to the given writer.
     */
    void writeGetMethods(JClassType target, SourceWriter writer) throws UnableToCompleteException {
        String targetType = target.getQualifiedSourceName();
        writer.println("public List<EventHandlerMethod<%s, ?>> getMethods() {", targetType);
        writer.indent();

        // Write a list that we will add all handlers to before returning
        writer.println("List<%1$s> methods = new LinkedList<%1$s>();",
                String.format("EventHandlerMethod<%s, ?>", targetType));

        // Iterate over each method in the target, looking for methods annotated with @Subscribe
        for (JMethod method : target.getInheritableMethods()) {
            if (method.getAnnotation(Subscribe.class) == null) {
                continue;
            }
            checkValidity(target, method);

            // Generate a list of types that should be handled by this method. Normally, this is a single
            // type equal to the method's first argument. If the argument in a MultiEvent, this list of
            // types comes from the @EventTypes annotation on the parameter.
            final List<String> paramTypes = new LinkedList<String>();
            final boolean isMultiEvent;
            if (getFirstParameterType(method).equals(MultiEvent.class.getCanonicalName())) {
                isMultiEvent = true;
                for (Class<?> type : method.getParameters()[0].getAnnotation(EventTypes.class).value()) {
                    paramTypes.add(type.getCanonicalName());
                }
            } else {
                isMultiEvent = false;
                paramTypes.add(getFirstParameterType(method));
            }

            // Add an implementation of EventHandlerMethod to the list for each type this method handles
            for (String paramType : paramTypes) {
                writer.println("methods.add(new EventHandlerMethod<%s, %s>() {", targetType, paramType);
                writer.indent();
                {
                    // Implement invoke() by calling the method, first checking filters if provided
                    writer.println("public void invoke(%s instance, %s arg) {", targetType, paramType);
                    String invocation = String.format(
                            isMultiEvent ? "instance.%s(new MultiEvent(arg));" : "instance.%s(arg);",
                            method.getName());
                    if (method.getAnnotation(When.class) != null) {
                        writer.indentln("if (%s) { %s }", getFilter(method), invocation);
                    } else {
                        writer.indentln(invocation);
                    }
                    writer.println("}");

                    // Implement acceptsArgument using instanceof
                    writer.println("public boolean acceptsArgument(Object arg) {");
                    writer.indentln("return arg instanceof %s;", paramType);
                    writer.println("}");

                    // Implement getDispatchOrder as the inverse of the method's priority
                    writer.println("public int getDispatchOrder() {");
                    writer.indentln("return %d;",
                            method.getAnnotation(WithPriority.class) != null
                                    ? -method.getAnnotation(WithPriority.class).value()
                                    : 0);
                    writer.println("}");
                }
                writer.outdent();
                writer.println("});");
            }
        }

        // Return the list of EventHandlerMethods
        writer.println("return methods;");
        writer.outdent();
        writer.println("}");
    }

    private void checkValidity(JClassType target, JMethod method) throws UnableToCompleteException {
        // General checks for all methods annotated with @Subscribe
        if (method.getParameterTypes().length != 1) {
            logger.log(Type.ERROR,
                    String.format("Method %s.%s annotated with @Subscribe must take exactly one argument.",
                            target.getName(), method.getName()));
            throw new UnableToCompleteException();
        } else if (method.isPrivate()) {
            logger.log(Type.ERROR, String.format("Method %s.%s annotated with @Subscribe must not be private.",
                    target.getName(), method.getName()));
            throw new UnableToCompleteException();
        }

        if (method.getParameterTypes()[0].getQualifiedSourceName().equals(MultiEvent.class.getCanonicalName())) {
            // Checks specific to MultiEvents
            if (method.getParameters()[0].getAnnotation(EventTypes.class) == null) {
                logger.log(Type.ERROR,
                        String.format("MultiEvent in method %s.%s must be annotated with @EventTypes.",
                                target.getName(), method.getName()));
                throw new UnableToCompleteException();
            }

            // Ensure that no type is assignable to another type
            Class<?>[] classes = method.getParameters()[0].getAnnotation(EventTypes.class).value();
            for (Class<?> c1 : classes) {
                for (Class<?> c2 : classes) {
                    if (c1 != c2 && c1.isAssignableFrom(c2)) {
                        logger.log(Type.ERROR,
                                String.format("The type %s is redundant with the type %s in method %s.%s.",
                                        c2.getSimpleName(), c1.getSimpleName(), target.getName(),
                                        method.getName()));
                        throw new UnableToCompleteException();
                    }
                }
            }
        } else {
            // Checks for non-MultiEvents
            if (method.getParameters()[0].getAnnotation(EventTypes.class) != null) {
                logger.log(Type.ERROR,
                        String.format(
                                "@EventTypes must not be applied to a non-MultiEvent parameter in method %s.%s.",
                                target.getName(), method.getName()));
                throw new UnableToCompleteException();
            }
        }
    }

    // Returns a boolean expression that should be used to check whether to invoke the given event
    // handler, based on the filters applied to it
    private String getFilter(JMethod method) throws UnableToCompleteException {
        StringBuilder predicate = new StringBuilder();
        When annotation = method.getAnnotation(When.class);
        boolean first = true;
        for (Class<?> filter : annotation.value()) {
            if (!classHasZeroArgConstructor(filter)) {
                logger.log(Type.ERROR,
                        String.format(
                                "Class %s extending EventFilter must define a public zero-argument constructor.",
                                filter.getSimpleName()));
                throw new UnableToCompleteException();
            }
            if (!first) {
                predicate.append(" && ");
            }
            first = false;
            predicate.append(String.format("new %s().accepts(instance, arg)", filter.getCanonicalName()));
        }
        return predicate.toString();
    }

    // Returns the type of the first parameter to the given method, boxed appropriately
    private String getFirstParameterType(JMethod method) {
        // If the parameter type is primitive, box it
        JType type = method.getParameterTypes()[0];
        if (type.isPrimitive() != null) {
            if (type.isPrimitive() == JPrimitiveType.BOOLEAN) {
                return Boolean.class.getName();
            } else if (type.isPrimitive() == JPrimitiveType.BYTE) {
                return Byte.class.getName();
            } else if (type.isPrimitive() == JPrimitiveType.CHAR) {
                return Character.class.getName();
            } else if (type.isPrimitive() == JPrimitiveType.DOUBLE) {
                return Double.class.getName();
            } else if (type.isPrimitive() == JPrimitiveType.FLOAT) {
                return Float.class.getName();
            } else if (type.isPrimitive() == JPrimitiveType.INT) {
                return Integer.class.getName();
            } else if (type.isPrimitive() == JPrimitiveType.LONG) {
                return Long.class.getName();
            } else if (type.isPrimitive() == JPrimitiveType.SHORT) {
                return Short.class.getName();
            }
        }

        // Otherwise return the fully-qualified type name
        return type.getQualifiedSourceName();
    }

    private boolean classHasZeroArgConstructor(Class<?> clazz) {
        try {
            for (Constructor<?> s : clazz.getConstructors()) {
                if (s.getParameterTypes().length == 0) {
                    return true;
                }
            }
            return false;
        } catch (SecurityException e) {
            throw new RuntimeException(e);
        }
    }
}