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

org.glowroot.weaving.AdviceBuilder Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2012-2015 the original author or authors.
 *
 * 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.glowroot.weaving;

import java.lang.annotation.Annotation;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.List;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import org.glowroot.shaded.google.common.base.Joiner;
import org.glowroot.shaded.google.common.collect.ImmutableList;
import org.glowroot.shaded.google.common.collect.ImmutableMap;
import org.glowroot.shaded.google.common.collect.Lists;
import org.glowroot.shaded.objectweb.asm.Type;
import org.glowroot.shaded.objectweb.asm.commons.Method;

import org.glowroot.plugin.api.weaving.BindClassMeta;
import org.glowroot.plugin.api.weaving.BindMethodMeta;
import org.glowroot.plugin.api.weaving.BindMethodName;
import org.glowroot.plugin.api.weaving.BindOptionalReturn;
import org.glowroot.plugin.api.weaving.BindParameter;
import org.glowroot.plugin.api.weaving.BindParameterArray;
import org.glowroot.plugin.api.weaving.BindReceiver;
import org.glowroot.plugin.api.weaving.BindReturn;
import org.glowroot.plugin.api.weaving.BindThrowable;
import org.glowroot.plugin.api.weaving.BindTraveler;
import org.glowroot.plugin.api.weaving.IsEnabled;
import org.glowroot.plugin.api.weaving.OnAfter;
import org.glowroot.plugin.api.weaving.OnBefore;
import org.glowroot.plugin.api.weaving.OnReturn;
import org.glowroot.plugin.api.weaving.OnThrow;
import org.glowroot.plugin.api.weaving.Pointcut;

import static org.glowroot.shaded.google.common.base.Preconditions.checkNotNull;

public class AdviceBuilder {

    private static final ImmutableList> isEnabledBindAnnotationTypes =
            ImmutableList.of(BindReceiver.class, BindParameter.class, BindParameterArray.class,
                    BindMethodName.class, BindClassMeta.class, BindMethodMeta.class);
    private static final ImmutableList> onBeforeBindAnnotationTypes =
            ImmutableList.of(BindReceiver.class, BindParameter.class, BindParameterArray.class,
                    BindMethodName.class, BindClassMeta.class, BindMethodMeta.class);
    private static final ImmutableList> onReturnBindAnnotationTypes =
            ImmutableList.of(BindReceiver.class, BindParameter.class, BindParameterArray.class,
                    BindMethodName.class, BindReturn.class, BindOptionalReturn.class,
                    BindTraveler.class, BindClassMeta.class, BindMethodMeta.class);
    private static final ImmutableList> onThrowBindAnnotationTypes =
            ImmutableList.of(BindReceiver.class, BindParameter.class, BindParameterArray.class,
                    BindMethodName.class, BindThrowable.class, BindTraveler.class,
                    BindClassMeta.class, BindMethodMeta.class);
    private static final ImmutableList> onAfterBindAnnotationTypes =
            ImmutableList.of(BindReceiver.class, BindParameter.class, BindParameterArray.class,
                    BindMethodName.class, BindTraveler.class, BindClassMeta.class,
                    BindMethodMeta.class);

    private static final ImmutableMap, ParameterKind> parameterKindMap =
            new ImmutableMap.Builder, ParameterKind>()
                    .put(BindReceiver.class, ParameterKind.RECEIVER)
                    .put(BindParameter.class, ParameterKind.METHOD_ARG)
                    .put(BindParameterArray.class, ParameterKind.METHOD_ARG_ARRAY)
                    .put(BindMethodName.class, ParameterKind.METHOD_NAME)
                    .put(BindReturn.class, ParameterKind.RETURN)
                    .put(BindOptionalReturn.class, ParameterKind.OPTIONAL_RETURN)
                    .put(BindThrowable.class, ParameterKind.THROWABLE)
                    .put(BindTraveler.class, ParameterKind.TRAVELER)
                    .put(BindClassMeta.class, ParameterKind.CLASS_META)
                    .put(BindMethodMeta.class, ParameterKind.METHOD_META)
                    .build();

    private final Advice.Builder builder = Advice.builder();

    private final @Nullable Class adviceClass;
    private final @Nullable LazyDefinedClass lazyAdviceClass;

    private boolean hasIsEnabledAdvice;
    private boolean hasOnBeforeAdvice;
    private boolean hasOnReturnAdvice;
    private boolean hasOnThrowAdvice;
    private boolean hasOnAfterAdvice;

    public AdviceBuilder(Class adviceClass, boolean reweavable) {
        this.adviceClass = adviceClass;
        this.lazyAdviceClass = null;
        builder.reweavable(reweavable);
    }

    public AdviceBuilder(LazyDefinedClass lazyAdviceClass, boolean reweavable) {
        this.adviceClass = null;
        this.lazyAdviceClass = lazyAdviceClass;
        builder.reweavable(reweavable);
    }

    public Advice build() throws Exception {
        Class adviceClass = this.adviceClass;
        if (adviceClass == null) {
            // safe check, if adviceClass is null then lazyAdviceClass is non-null
            checkNotNull(lazyAdviceClass);
            ClassLoader tempClassLoader =
                    AccessController.doPrivileged(new PrivilegedAction() {
                        @Override
                        public ClassLoader run() {
                            return new URLClassLoader(new URL[0]);
                        }
                    });
            adviceClass = ClassLoaders.defineClass(lazyAdviceClass, tempClassLoader);
        }
        Pointcut pointcut = adviceClass.getAnnotation(Pointcut.class);
        checkState(pointcut != null, "Class has no @Pointcut annotation");
        checkNotNull(pointcut);
        builder.pointcut(pointcut);
        builder.adviceType(Type.getType(adviceClass));
        if (pointcut.declaringClassName().equals("")) {
            builder.pointcutDeclaringClassName(pointcut.className());
            builder.pointcutDeclaringClassNamePattern(buildPattern(pointcut.className()));
            builder.pointcutTargetClassName(null);
            builder.pointcutTargetClassNamePattern(null);
        } else {
            builder.pointcutDeclaringClassName(pointcut.declaringClassName());
            builder.pointcutDeclaringClassNamePattern(buildPattern(pointcut.declaringClassName()));
            builder.pointcutTargetClassName(pointcut.className());
            builder.pointcutTargetClassNamePattern(buildPattern(pointcut.className()));
        }
        builder.pointcutMethodNamePattern(buildPattern(pointcut.methodName()));
        for (java.lang.reflect.Method method : adviceClass.getMethods()) {
            if (method.isAnnotationPresent(IsEnabled.class)) {
                initIsEnabledAdvice(adviceClass, method);
            } else if (method.isAnnotationPresent(OnBefore.class)) {
                initOnBeforeAdvice(adviceClass, method);
            } else if (method.isAnnotationPresent(OnReturn.class)) {
                initOnReturnAdvice(adviceClass, method);
            } else if (method.isAnnotationPresent(OnThrow.class)) {
                initOnThrowAdvice(adviceClass, method);
            } else if (method.isAnnotationPresent(OnAfter.class)) {
                initOnAfterAdvice(adviceClass, method);
            }
        }
        return builder.build();
    }

    private void initIsEnabledAdvice(Class adviceClass, java.lang.reflect.Method method)
            throws AdviceConstructionException {
        checkState(!hasIsEnabledAdvice,
                "@Pointcut '" + adviceClass.getName() + "' has more than one @IsEnabled method");
        Method asmMethod = Method.getMethod(method);
        checkState(asmMethod.getReturnType().getSort() == Type.BOOLEAN,
                "@IsEnabled method must return boolean");
        builder.isEnabledAdvice(asmMethod);
        List parameters = getAdviceParameters(method.getParameterAnnotations(),
                method.getParameterTypes(), isEnabledBindAnnotationTypes, IsEnabled.class);
        builder.addAllIsEnabledParameters(parameters);
        hasIsEnabledAdvice = true;
    }

    private void initOnBeforeAdvice(Class adviceClass, java.lang.reflect.Method method)
            throws AdviceConstructionException {
        checkState(!hasOnBeforeAdvice,
                "@Pointcut '" + adviceClass.getName() + "' has more than one @OnBefore method");
        Method onBeforeAdvice = Method.getMethod(method);
        builder.onBeforeAdvice(onBeforeAdvice);
        List parameters = getAdviceParameters(method.getParameterAnnotations(),
                method.getParameterTypes(), onBeforeBindAnnotationTypes, OnBefore.class);
        builder.addAllOnBeforeParameters(parameters);
        if (onBeforeAdvice.getReturnType().getSort() != Type.VOID) {
            builder.travelerType(onBeforeAdvice.getReturnType());
        }
        hasOnBeforeAdvice = true;
    }

    private void initOnReturnAdvice(Class adviceClass, java.lang.reflect.Method method)
            throws AdviceConstructionException {
        checkState(!hasOnReturnAdvice,
                "@Pointcut '" + adviceClass.getName() + "' has more than one @OnReturn method");
        List parameters = getAdviceParameters(method.getParameterAnnotations(),
                method.getParameterTypes(), onReturnBindAnnotationTypes, OnReturn.class);
        for (int i = 1; i < parameters.size(); i++) {
            checkState(parameters.get(i).kind() != ParameterKind.RETURN,
                    "@BindReturn must be the first argument to @OnReturn");
            checkState(parameters.get(i).kind() != ParameterKind.OPTIONAL_RETURN,
                    "@BindOptionalReturn must be the first argument to @OnReturn");
        }
        builder.onReturnAdvice(Method.getMethod(method));
        builder.addAllOnReturnParameters(parameters);
        hasOnReturnAdvice = true;
    }

    private void initOnThrowAdvice(Class adviceClass, java.lang.reflect.Method method)
            throws AdviceConstructionException {
        checkState(!hasOnThrowAdvice,
                "@Pointcut '" + adviceClass.getName() + "' has more than one @OnThrow method");
        List parameters = getAdviceParameters(method.getParameterAnnotations(),
                method.getParameterTypes(), onThrowBindAnnotationTypes, OnThrow.class);
        for (int i = 1; i < parameters.size(); i++) {
            checkState(parameters.get(i).kind() != ParameterKind.THROWABLE,
                    "@BindThrowable must be the first argument to @OnThrow");
        }
        Method asmMethod = Method.getMethod(method);
        checkState(asmMethod.getReturnType().getSort() == Type.VOID,
                "@OnThrow method must return void (for now)");
        builder.onThrowAdvice(asmMethod);
        builder.addAllOnThrowParameters(parameters);
        hasOnThrowAdvice = true;
    }

    private void initOnAfterAdvice(Class adviceClass, java.lang.reflect.Method method)
            throws AdviceConstructionException {
        checkState(!hasOnAfterAdvice,
                "@Pointcut '" + adviceClass.getName() + "' has more than one @OnAfter method");
        Method asmMethod = Method.getMethod(method);
        checkState(asmMethod.getReturnType().getSort() == Type.VOID,
                "@OnAfter method must return void");
        builder.onAfterAdvice(asmMethod);
        List parameters = getAdviceParameters(method.getParameterAnnotations(),
                method.getParameterTypes(), onAfterBindAnnotationTypes, OnAfter.class);
        builder.addAllOnAfterParameters(parameters);
        hasOnAfterAdvice = true;
    }

    private static void checkState(boolean condition, String message)
            throws AdviceConstructionException {
        if (!condition) {
            throw new AdviceConstructionException(message);
        }
    }

    private static @Nullable Pattern buildPattern(String maybePattern) {
        if (maybePattern.startsWith("/") && maybePattern.endsWith("/")) {
            // full regex power
            return Pattern.compile(maybePattern.substring(1, maybePattern.length() - 1));
        }
        // limited regex, | and *, should be used whenever possible over full regex since
        // . and $ are common in class names
        if (maybePattern.contains("|")) {
            String[] parts = maybePattern.split("\\|");
            for (int i = 0; i < parts.length; i++) {
                parts[i] = buildSimplePattern(parts[i]);
            }
            return Pattern.compile(Joiner.on('|').join(parts));
        }
        if (maybePattern.contains("*")) {
            return Pattern.compile(buildSimplePattern(maybePattern));
        }
        return null;
    }

    public static String buildSimplePattern(String part) {
        // convert * into .* and quote the rest of the text using \Q...\E
        String pattern = "\\Q" + part.replace("*", "\\E.*\\Q") + "\\E";
        // strip off unnecessary \\Q\\E in case * appeared at beginning or end of part
        return pattern.replace("\\Q\\E", "");
    }

    private static List getAdviceParameters(Annotation[][] parameterAnnotations,
            Class[] parameterTypes,
            ImmutableList> validBindAnnotationTypes,
            Class adviceAnnotationType) throws AdviceConstructionException {

        List parameters = Lists.newArrayList();
        for (int i = 0; i < parameterAnnotations.length; i++) {
            Class validBindAnnotationType =
                    getValidBindAnnotationType(parameterAnnotations[i], validBindAnnotationTypes);
            if (validBindAnnotationType == null) {
                // no valid bind annotations found, provide a good error message
                List validBindAnnotationNames = Lists.newArrayList();
                for (Class annotationType : validBindAnnotationTypes) {
                    validBindAnnotationNames.add("@" + annotationType.getSimpleName());
                }
                throw new AdviceConstructionException("All parameters to @"
                        + adviceAnnotationType.getSimpleName() + " must be annotated with one"
                        + " of " + Joiner.on(", ").join(validBindAnnotationNames));
            }
            parameters.add(getAdviceParameter(validBindAnnotationType, parameterTypes[i]));
        }
        return parameters;
    }

    private static @Nullable Class getValidBindAnnotationType(
            Annotation[] parameterAnnotations,
            ImmutableList> validBindAnnotationTypes)
                    throws AdviceConstructionException {

        Class foundBindAnnotationType = null;
        for (Annotation annotation : parameterAnnotations) {
            Class annotationType = annotation.annotationType();
            if (!parameterKindMap.containsKey(annotationType)) {
                continue;
            }
            checkState(foundBindAnnotationType == null,
                    "Multiple annotations found on a single parameter");
            checkState(validBindAnnotationTypes.contains(annotationType),
                    "Annotation '" + annotationType.getName() + "' found in an invalid location");
            foundBindAnnotationType = annotationType;
        }
        return foundBindAnnotationType;
    }

    private static AdviceParameter getAdviceParameter(
            Class validBindAnnotationType, Class parameterType)
                    throws AdviceConstructionException {

        checkState(
                validBindAnnotationType != BindMethodName.class
                        || parameterType.isAssignableFrom(String.class),
                "@BindMethodName parameter type must be"
                        + " java.lang.String (or super type of java.lang.String)");
        checkState(
                validBindAnnotationType != BindThrowable.class
                        || parameterType.isAssignableFrom(Throwable.class),
                "@BindMethodName parameter type must be"
                        + " java.lang.Throwable (or super type of java.lang.Throwable)");
        ParameterKind parameterKind = parameterKindMap.get(validBindAnnotationType);
        // parameterKind should never be null since all bind annotations have a mapping in
        // parameterKindMap
        checkNotNull(parameterKind,
                "Annotation not found in parameterKindMap: " + validBindAnnotationType.getName());
        return AdviceParameter.builder()
                .kind(parameterKind)
                .type(Type.getType(parameterType))
                .build();
    }

    @SuppressWarnings("serial")
    private static class AdviceConstructionException extends Exception {
        private AdviceConstructionException(@Nullable String message) {
            super(message);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy