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

org.jboss.logging.processor.apt.MessageMethodBuilder Maven / Gradle / Ivy

/*
 * JBoss, Home of Professional Open Source.
 *
 * Copyright 2023 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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.jboss.logging.processor.apt;

import static org.jboss.logging.processor.util.ElementHelper.isAnnotatedWith;
import static org.jboss.logging.processor.util.Objects.HashCodeBuilder;
import static org.jboss.logging.processor.util.Objects.ToStringBuilder;
import static org.jboss.logging.processor.util.Objects.areEqual;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.function.Supplier;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;

import org.jboss.logging.annotations.Cause;
import org.jboss.logging.annotations.Field;
import org.jboss.logging.annotations.LogMessage;
import org.jboss.logging.annotations.Message;
import org.jboss.logging.annotations.Message.Format;
import org.jboss.logging.annotations.Param;
import org.jboss.logging.annotations.Pos;
import org.jboss.logging.annotations.Property;
import org.jboss.logging.annotations.Suppressed;
import org.jboss.logging.annotations.Transform;
import org.jboss.logging.processor.model.LoggerMessageMethod;
import org.jboss.logging.processor.model.MessageMethod;
import org.jboss.logging.processor.model.Parameter;
import org.jboss.logging.processor.model.ReturnType;
import org.jboss.logging.processor.model.ThrowableType;
import org.jboss.logging.processor.util.Comparison;
import org.jboss.logging.processor.util.ElementHelper;
import org.jboss.logging.processor.util.Expressions;

/**
 * Date: 29.07.2011
 *
 * @author James R. Perkins
 */
final class MessageMethodBuilder {

    private static final String MESSAGE_METHOD_SUFFIX = "$str";
    private final List methods;
    private final ProcessingEnvironment processingEnv;
    private final Properties expressionProperties;

    private MessageMethodBuilder(final ProcessingEnvironment processingEnv, final Properties expressionProperties) {
        this.processingEnv = processingEnv;
        this.expressionProperties = expressionProperties;
        methods = new LinkedList<>();
    }

    MessageMethodBuilder add(final Collection methods) {
        this.methods.addAll(methods);
        return this;
    }

    Set build() {
        final Elements elements = processingEnv.getElementUtils();
        final Set result = new LinkedHashSet<>();
        for (ExecutableElement elementMethod : methods) {
            final AptMessageMethod resultMethod = createMessageMethod(elements, elementMethod);
            resultMethod.inheritsMessage = inheritsMessage(methods, elementMethod);
            resultMethod.message = findMessage(methods, elementMethod);
            resultMethod.isOverloaded = isOverloaded(methods, elementMethod);
            for (TypeMirror thrownType : elementMethod.getThrownTypes()) {
                resultMethod.thrownTypes.add(ThrowableTypeFactory.of(processingEnv, thrownType));
            }

            // Create a list of parameters
            for (Parameter parameter : ParameterFactory.of(processingEnv, resultMethod.method)) {
                resultMethod.add(parameter);
            }
            // Check to see if the method is overloaded
            if (resultMethod.isOverloaded()) {
                resultMethod.messageMethodName = resultMethod.name() + resultMethod.formatParameterCount()
                        + MESSAGE_METHOD_SUFFIX;
                resultMethod.translationKey = resultMethod.name() + "." + resultMethod.formatParameterCount();
            } else {
                resultMethod.messageMethodName = resultMethod.name() + MESSAGE_METHOD_SUFFIX;
                resultMethod.translationKey = resultMethod.name();
            }
            // Set the return type
            resultMethod.returnType = ReturnTypeFactory.of(processingEnv, elementMethod.getReturnType(), resultMethod);
            result.add(resultMethod);
        }
        return Collections.unmodifiableSet(result);
    }

    private MessageMethod.Message findMessage(final Collection methods, final ExecutableElement method) {
        AptMessage result = null;
        Message message = method.getAnnotation(Message.class);
        if (message != null) {
            result = new AptMessage(message, expressionProperties);
            result.hasId = hasMessageId(message);
            result.inheritsId = message.id() == Message.INHERIT;
            if (result.inheritsId()) {
                result.id = findMessageId(methods, method);
                if (result.id > 0) {
                    result.hasId = true;
                }
            } else {
                result.id = message.id();
            }
        } else {
            final Collection allMethods = findByName(methods, method);
            for (ExecutableElement m : allMethods) {
                message = m.getAnnotation(Message.class);
                if (message != null) {
                    result = new AptMessage(message, expressionProperties);
                    result.hasId = hasMessageId(message);
                    result.inheritsId = message.id() == Message.INHERIT;
                    if (result.inheritsId()) {
                        result.id = findMessageId(methods, m);
                        if (result.id > 0) {
                            result.hasId = true;
                        }
                    } else {
                        result.id = message.id();
                    }
                    break;
                }
            }
        }
        return result;
    }

    private int findMessageId(final Collection methods, final ExecutableElement method) {
        int result = -2;
        final Collection allMethods = findByName(methods, method.getSimpleName());
        for (ExecutableElement m : allMethods) {
            final Message message = m.getAnnotation(Message.class);
            if (message != null) {
                if (message.id() != Message.INHERIT) {
                    result = message.id();
                }
            }
        }
        return result;
    }

    private boolean hasMessageId(final Message message) {
        return message != null && (message.id() != Message.NONE && message.id() != Message.INHERIT);
    }

    private Collection findByName(final Collection methods,
            final ExecutableElement method) {
        final Name methodName = method.getSimpleName();
        final List result = new ArrayList<>();
        final int paramCount = parameterCount(method.getParameters());
        for (ExecutableElement m : methods) {
            if (methodName.equals(m.getSimpleName()) && parameterCount(m.getParameters()) == paramCount) {
                result.add(m);
            }
        }
        return result;
    }

    /**
     * Returns a collection of methods with the same name.
     *
     * @param methods    the methods to process.
     * @param methodName the method name to findByName.
     *
     * @return a collection of methods with the same name.
     */
    private Collection findByName(final Collection methods, final Name methodName) {
        final List result = new ArrayList<>();
        for (ExecutableElement method : methods) {
            if (methodName.equals(method.getSimpleName())) {
                result.add(method);
            }
        }
        return result;
    }

    /**
     * Checks to see if the method has or inherits a {@link org.jboss.logging.annotations.Message}
     * annotation.
     *
     * @param methods the method to search.
     * @param method  the method to check.
     *
     * @return {@code true} if the method has or inherits a message annotation, otherwise {@code false}.
     */

    private boolean inheritsMessage(final Collection methods, final ExecutableElement method) {
        if (isAnnotatedWith(method, Message.class)) {
            return false;
        }
        final Collection allMethods = findByName(methods, method.getSimpleName());
        for (ExecutableElement m : allMethods) {
            if (isAnnotatedWith(m, Message.class)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks to see if the method is overloaded. An overloaded method has a different parameter count based on the
     * format parameters only. Parameters annotated with {@link org.jboss.logging.annotations.Cause} or
     * {@link org.jboss.logging.annotations.Param}
     * are not counted.
     *
     * @param methods the method to search.
     * @param method  the method to check.
     *
     * @return {@code true} if the method is overloaded, otherwise {@code false}.
     */
    private boolean isOverloaded(final Collection methods, final ExecutableElement method) {
        final Collection allMethods = findByName(methods, method.getSimpleName());
        for (ExecutableElement m : allMethods) {
            if (method.getSimpleName().equals(m.getSimpleName())
                    && parameterCount(method.getParameters()) != parameterCount(m.getParameters())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the number of parameters excluding the {@link org.jboss.logging.annotations.Cause} parameter
     * and any {@link org.jboss.logging.annotations.Param} parameters if found.
     *
     * @param params the parameters to get the count for.
     *
     * @return the number of parameters.
     */
    private int parameterCount(final Collection params) {
        int result = params.size();
        boolean hasCause = false;
        for (VariableElement param : params) {
            if (isAnnotatedWith(param, Param.class) || isAnnotatedWith(param, Field.class) ||
                    isAnnotatedWith(param, Property.class) || isAnnotatedWith(param, Suppressed.class)) {
                --result;
            }
            if (isAnnotatedWith(param, Cause.class)) {
                hasCause = true;
            }
        }
        return (result - (hasCause ? 1 : 0));
    }

    static MessageMethodBuilder create(final ProcessingEnvironment processingEnv) {
        return create(processingEnv, new Properties());
    }

    static MessageMethodBuilder create(final ProcessingEnvironment processingEnv, final Properties expressionProperties) {
        return new MessageMethodBuilder(processingEnv, expressionProperties);
    }

    private static AptMessageMethod createMessageMethod(final Elements elements, final ExecutableElement elementMethod) {
        if (elementMethod.getAnnotation(LogMessage.class) == null) {
            return new AptMessageMethod(elements, elementMethod);
        }
        return new AptLoggerMessageMethod(elements, elementMethod);
    }

    /**
     * An implementation for the MessageMethod interface.
     */
    private static class AptMessageMethod implements MessageMethod {

        final Elements elements;
        final Map> parameters;
        final Set thrownTypes;
        final ExecutableElement method;
        ReturnType returnType;
        Parameter cause;
        boolean inheritsMessage;
        boolean isOverloaded;
        Message message;
        String messageMethodName;
        String translationKey;
        int formatParameterCount;

        /**
         * Private constructor for the
         *
         * @param elements the elements utility.
         * @param method   the method to describe.
         */
        AptMessageMethod(final Elements elements, final ExecutableElement method) {
            this.elements = elements;
            this.method = method;
            inheritsMessage = false;
            isOverloaded = false;
            parameters = new HashMap<>();
            thrownTypes = new LinkedHashSet<>();
            formatParameterCount = 0;
        }

        void add(final Parameter parameter) {
            if (parameter.isFormatParameter()) {
                if (parameter.isAnnotatedWith(Pos.class)) {
                    formatParameterCount += parameter.getAnnotation(Pos.class).value().length;
                } else {
                    formatParameterCount++;
                }
            }
            if (parameters.containsKey(null)) {
                parameters.get(null).add(parameter);
            } else {
                final Set any = new LinkedHashSet<>();
                any.add(parameter);
                parameters.put(null, any);
            }
            for (AnnotationMirror a : parameter.getAnnotationMirrors()) {
                if (parameters.containsKey(a.getAnnotationType())) {
                    parameters.get(a.getAnnotationType()).add(parameter);
                } else {
                    final Set set = new LinkedHashSet<>();
                    set.add(parameter);
                    parameters.put(a.getAnnotationType(), set);
                }
            }
            if (parameter.isAnnotatedWith(Cause.class)) {
                cause = parameter;
            }
        }

        @Override
        public String name() {
            return method.getSimpleName().toString();
        }

        @Override
        public Set parameters() {
            if (parameters.containsKey(null)) {
                return Collections.unmodifiableSet(parameters.get(null));
            }
            return Collections.emptySet();
        }

        @Override
        public Set parametersAnnotatedWith(final Class annotation) {
            final TypeElement type = ElementHelper.toTypeElement(elements, annotation);
            return (type != null && parameters.containsKey(type.asType()))
                    ? Collections.unmodifiableSet(parameters.get(type.asType()))
                    : Collections.emptySet();
        }

        @Override
        public ReturnType returnType() {
            return returnType;
        }

        @Override
        public Set thrownTypes() {
            return Collections.unmodifiableSet(thrownTypes);
        }

        @Override
        public Message message() {
            return message;
        }

        @Override
        public boolean inheritsMessage() {
            return inheritsMessage;
        }

        @Override
        public String messageMethodName() {
            return messageMethodName;
        }

        @Override
        public String translationKey() {
            return translationKey;
        }

        @Override
        public boolean hasCause() {
            return cause != null;
        }

        @Override
        public boolean isOverloaded() {
            return isOverloaded;
        }

        @Override
        public Parameter cause() {
            return cause;
        }

        @Override
        public int formatParameterCount() {
            return formatParameterCount;
        }

        @Override
        public int hashCode() {
            return HashCodeBuilder.builder()
                    .add(name())
                    .add(parameters())
                    .add(returnType()).toHashCode();
        }

        @Override
        public boolean equals(final Object obj) {
            if (obj == this) {
                return true;
            }
            if (!(obj instanceof AptMessageMethod)) {
                return false;
            }
            final AptMessageMethod other = (AptMessageMethod) obj;
            return areEqual(name(), other.name()) &&
                    areEqual(parameters, parameters) &&
                    areEqual(returnType, other.returnType);
        }

        @Override
        public String toString() {
            return ToStringBuilder.of(this)
                    .add("name", name())
                    .add("returnType", returnType())
                    .add("parameters", parameters()).toString();
        }

        @Override
        public ExecutableElement getDelegate() {
            return method;
        }

        @Override
        public int compareTo(final MessageMethod o) {
            int result = name().compareTo(o.name());
            result = (result != Comparison.EQUAL) ? result : returnType.name().compareTo(o.returnType().name());
            // Size does matter
            result = (result != Comparison.EQUAL) ? result : parameters().size() - o.parameters().size();
            if (result == Comparison.EQUAL) {
                // Check element by element
                final Iterator params1 = parameters().iterator();
                final Iterator params2 = o.parameters().iterator();
                while (params1.hasNext()) {
                    if (params2.hasNext()) {
                        final Parameter param1 = params1.next();
                        final Parameter param2 = params2.next();
                        result = param1.compareTo(param2);
                    } else {
                        result = Comparison.GREATER;
                    }
                    // Short circuit
                    if (result != Comparison.EQUAL)
                        break;
                }
            }
            return result;
        }

        @Override
        public String getComment() {
            return elements.getDocComment(method);
        }
    }

    private static class AptLoggerMessageMethod extends AptMessageMethod implements LoggerMessageMethod {

        /**
         * Private constructor for the
         *
         * @param elements the elements utility.
         * @param method   the method to describe.
         */
        AptLoggerMessageMethod(final Elements elements, final ExecutableElement method) {
            super(elements, method);
        }

        @Override
        public String loggerMethod() {
            switch (message.format()) {
                case MESSAGE_FORMAT:
                    return "logv";
                case NO_FORMAT:
                    return "log";
                case PRINTF:
                    return "logf";
                default:
                    // Should never be hit
                    return "log";
            }
        }

        @Override
        public String logLevel() {
            final LogMessage logMessage = method.getAnnotation(LogMessage.class);
            return logMessage.level().name();
        }

        @Override
        public boolean wrapInEnabledCheck() {
            if (!parametersAnnotatedWith(Transform.class).isEmpty()) {
                return true;
            }
            for (Parameter parameter : parameters()) {
                if (parameter.isSubtypeOf(Supplier.class)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public String toString() {
            return ToStringBuilder.of(this)
                    .add("name", name())
                    .add("returnType", returnType())
                    .add("parameters", parameters())
                    .add("loggerMethod", loggerMethod()).toString();
        }
    }

    private static class AptMessage implements MessageMethod.Message {

        private final Message message;
        private final String messageValue;
        private int id;
        private boolean hasId;
        private boolean inheritsId;

        private AptMessage(final Message message, final Properties expressionProperties) {
            this.message = message;
            if (expressionProperties.isEmpty()) {
                this.messageValue = message.value();
            } else {
                this.messageValue = Expressions.resolve(expressionProperties, message.value());
            }
        }

        @Override
        public int id() {
            return id;
        }

        @Override
        public boolean hasId() {
            return hasId;
        }

        @Override
        public boolean inheritsId() {
            return inheritsId;
        }

        @Override
        public String value() {
            return messageValue;
        }

        @Override
        public Format format() {
            return message.format();
        }

        @Override
        public String toString() {
            return ToStringBuilder.of(this)
                    .add("hasId", hasId)
                    .add("id", id())
                    .add("inheritsId", inheritsId)
                    .add("value", value())
                    .add("formatType", format()).toString();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy