com.appland.appmap.transform.annotations.Hook Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of appmap-agent Show documentation
Show all versions of appmap-agent Show documentation
Inspect and record the execution of Java for use with App Land
package com.appland.appmap.transform.annotations;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.tinylog.TaggedLogger;
import com.appland.appmap.config.AppMapConfig;
import com.appland.appmap.output.v1.Parameters;
import com.appland.appmap.record.EventTemplateRegistry;
import com.appland.appmap.util.AppMapClassPool;
import com.appland.appmap.util.Logger;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;
import javassist.NotFoundException;
public class Hook {
private static final TaggedLogger logger = AppMapConfig.getLogger(null);
private static final EventTemplateRegistry eventTemplateRegistry = EventTemplateRegistry.get();
private final static List> requiredHookSystemFactories =
new ArrayList>() {{
add(HookAnnotatedSystem::from);
add(HookClassSystem::from);
add(HookConditionSystem::from);
}};
private final static List> optionalSystemFactories =
new ArrayList>() {{
add(ExcludeReceiverSystem::from);
add(ArgumentArraySystem::from);
}};
private final SourceMethodSystem sourceSystem;
private final List optionalSystems;
private final Parameters staticParameters = new Parameters();
private final Parameters hookParameters;
private final CtBehavior hookBehavior;
private String uniqueKey = "";
private Hook( SourceMethodSystem sourceSystem,
List optionalSystems,
CtBehavior hookBehavior) {
this.sourceSystem = sourceSystem;
this.optionalSystems = optionalSystems;
this.hookBehavior = hookBehavior;
this.hookParameters = new Parameters(hookBehavior);
this.uniqueKey = (String) AnnotationUtil.getValue(hookBehavior, Unique.class, "");
this.buildParameters();
}
/**
* Creates a Hook from code behavior.
*
* @return If hookBehavior is a valid hook, return a new Hook object. Otherwise, null.
*/
public static Hook from(CtBehavior hookBehavior) {
SourceMethodSystem sourceSystem = null;
for (Function factoryFn : requiredHookSystemFactories) {
sourceSystem = (SourceMethodSystem) factoryFn.apply(hookBehavior);
if (sourceSystem != null) {
break;
}
}
if (sourceSystem == null) {
return null;
}
List optionalSystems = optionalSystemFactories
.stream()
.map(factoryFn -> factoryFn.apply(hookBehavior))
.filter(Objects::nonNull)
.collect(Collectors.toList());
Hook hook = new Hook(sourceSystem, optionalSystems, hookBehavior);
for (ISystem optionalSystem : optionalSystems) {
if (!optionalSystem.validate(hook)) {
Logger.println("hook "
+ hook
+ " failed validation from "
+ optionalSystem.getClass().getSimpleName());
return null;
}
}
return hook;
}
public void buildParameters() {
this.sourceSystem.mutateStaticParameters(this.hookBehavior, this.staticParameters);
this.optionalSystems
.stream()
.sorted(Comparator.comparingInt(ISystem::getParameterPriority))
.forEach(system -> system.mutateStaticParameters(this.hookBehavior, this.staticParameters));
}
public Parameters getRuntimeParameters(HookBinding binding) {
Parameters runtimeParameters = this.staticParameters.clone();
Stream.concat(Stream.of(this.sourceSystem), this.optionalSystems.stream())
.sorted(Comparator.comparingInt(ISystem::getParameterPriority))
.forEach(system -> {
system.mutateRuntimeParameters(binding, runtimeParameters);
});
return runtimeParameters;
}
public HookSite prepare(CtBehavior targetBehavior) {
if (targetBehavior instanceof CtConstructor) {
return null;
}
Map matchResult = new HashMap();
if (!this.sourceSystem.match(targetBehavior, matchResult)) {
return null;
}
String[] labels = (String[])matchResult.getOrDefault("labels", new String[0]);
Integer behaviorOrdinal = eventTemplateRegistry.register(targetBehavior, labels);
if (behaviorOrdinal < 0) {
return null;
}
HookBinding binding = new HookBinding(this, targetBehavior, behaviorOrdinal);
for (ISystem system : this.optionalSystems) {
if (!system.validate(binding)) {
return null;
}
}
Parameters runtimeParameters = this.getRuntimeParameters(binding);
return new HookSite(this, behaviorOrdinal, runtimeParameters);
}
public static void apply(CtBehavior targetBehavior, List hookSites) {
final CtClass returnType = getReturnType(targetBehavior);
final Boolean returnsVoid = (returnType == CtClass.voidType);
final String[] invocations = new String[3];
for (HookSite hookSite : hookSites) {
final Integer index = hookSite.getMethodEvent().getIndex();
if (invocations[index] == null) {
invocations[index] = hookSite.getHookInvocation();
} else {
invocations[index] += hookSite.getHookInvocation();
}
}
final String uniqueLocks = hookSites
.stream()
.map(HookSite::getUniqueKey)
.filter(uk -> !uk.isEmpty())
.distinct()
.map(uniqueKey -> (""
+ "com.appland.appmap.process.ThreadLock.current().lockUnique(\""
+ uniqueKey
+ "\");"))
.collect(Collectors.joining("\n"));
try {
targetBehavior.insertBefore(
beforeSrcBlock(uniqueLocks, invocations[MethodEvent.METHOD_INVOCATION.getIndex()])
);
targetBehavior.insertAfter(
afterSrcBlock(invocations[MethodEvent.METHOD_RETURN.getIndex()]));
ClassPool cp = AppMapClassPool.get();
if (returnsVoid) {
targetBehavior.addCatch("{"
+ "com.appland.appmap.process.ThreadLock.current().exit();"
+ "return;"
+ "}",
cp.get("com.appland.appmap.process.ExitEarly"));
} else if (!returnType.isPrimitive()) {
targetBehavior.addCatch("{"
+ "com.appland.appmap.process.ThreadLock.current().exit();"
+ "return ("
+ returnType.getName()
+ ") $e.getReturnValue();"
+ "}",
cp.get("com.appland.appmap.process.ExitEarly"));
}
targetBehavior.addCatch(
catchSrcBlock(invocations[MethodEvent.METHOD_EXCEPTION.getIndex()]),
cp.get("java.lang.Throwable"));
} catch (CannotCompileException e) {
logger.debug(e, "failed to compile {}.{}", targetBehavior.getDeclaringClass().getName(),
targetBehavior.getName());
} catch (NotFoundException e) {
logger.debug(e);
}
}
/* Concatenates potentially null strings with no delimeter. The return value
is guaranteed to be non-null.
*/
private static String safeConcatStrings(String... strs) {
return Arrays.stream(strs)
.filter(Objects::nonNull)
.collect(Collectors.joining());
}
private static String beforeSrcBlock(String... invocations) {
final String allInvocations = safeConcatStrings(invocations);
return "{"
+ "com.appland.appmap.process.ThreadLock.current().enter();"
+ allInvocations
+ "}";
}
private static String afterSrcBlock(String... invocations) {
final String allInvocations = safeConcatStrings(invocations);
return "{"
+ allInvocations
+ "com.appland.appmap.process.ThreadLock.current().exit();"
+ "}";
}
private static String catchSrcBlock(String... invocations) {
final String allInvocations = safeConcatStrings(invocations);
return "{"
+ allInvocations
+ "com.appland.appmap.process.ThreadLock.current().exit();"
+ "throw $e;"
+ "}";
}
public String getKey() {
return this.sourceSystem.getKey();
}
public String toString() {
return String.format("%s(%s)", this.sourceSystem.toString(), this.hookParameters.toString());
}
public String getUniqueKey() {
return this.uniqueKey;
}
public Parameters getParameters() {
return this.hookParameters;
}
public CtBehavior getBehavior() {
return this.hookBehavior;
}
public void validate() throws HookValidationException {
}
public void validate(CtBehavior behavior) throws HookValidationException {
}
public MethodEvent getMethodEvent() {
return this.sourceSystem.getMethodEvent();
}
public SourceMethodSystem getSourceSystem() {
return this.sourceSystem;
}
private static CtClass getReturnType(CtBehavior behavior) {
CtClass returnType = CtClass.voidType;
if (behavior instanceof CtMethod) {
try {
returnType = ((CtMethod) behavior).getReturnType();
} catch (NotFoundException e) {
logger.debug(e, "unknown return type");
}
}
return returnType;
}
public ISystem getSystem(Class extends ISystem> systemClass) {
for (ISystem system : this.optionalSystems) {
if (systemClass.isInstance(system)) {
return system;
}
}
return null;
}
public Integer getPosition() {
return sourceSystem.getHookPosition();
}
}