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 extends Annotation> adviceAnnotationType) throws AdviceConstructionException {
List parameters = Lists.newArrayList();
for (int i = 0; i < parameterAnnotations.length; i++) {
Class extends Annotation> validBindAnnotationType =
getValidBindAnnotationType(parameterAnnotations[i], validBindAnnotationTypes);
if (validBindAnnotationType == null) {
// no valid bind annotations found, provide a good error message
List validBindAnnotationNames = Lists.newArrayList();
for (Class extends Annotation> 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 extends Annotation> getValidBindAnnotationType(
Annotation[] parameterAnnotations,
ImmutableList> validBindAnnotationTypes)
throws AdviceConstructionException {
Class extends Annotation> foundBindAnnotationType = null;
for (Annotation annotation : parameterAnnotations) {
Class extends Annotation> 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 extends Annotation> 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);
}
}
}