/*
* Copyright 2013-2017 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.agent.live;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import com.google.common.base.Splitter;
import com.google.common.base.StandardSystemProperty;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import com.google.common.io.Resources;
import org.immutables.value.Value;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.glowroot.agent.weaving.AnalyzedWorld;
import org.glowroot.agent.weaving.ClassNames;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.objectweb.asm.Opcodes.ACC_NATIVE;
import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
import static org.objectweb.asm.Opcodes.ASM5;
// TODO remove items from classpathLocations and classNameLocations when class loaders are no longer
// present, e.g. in wildfly after undeploying an application
class ClasspathCache {
private static final Logger logger = LoggerFactory.getLogger(ClasspathCache.class);
private final AnalyzedWorld analyzedWorld;
private final @Nullable Instrumentation instrumentation;
@GuardedBy("this")
private final Set<Location> classpathLocations = Sets.newHashSet();
// using ImmutableMultimap because it is very space efficient
// this is not updated often so trading space efficiency for copying the entire map on update
@GuardedBy("this")
private ImmutableMultimap<String, Location> classNameLocations = ImmutableMultimap.of();
ClasspathCache(AnalyzedWorld analyzedWorld, @Nullable Instrumentation instrumentation) {
this.analyzedWorld = analyzedWorld;
this.instrumentation = instrumentation;
}
// using synchronization instead of concurrent structures in this cache to conserve memory
synchronized ImmutableList<String> getMatchingClassNames(String partialClassName, int limit) {
// update cache before proceeding
updateCache();
PartialClassNameMatcher matcher = new PartialClassNameMatcher(partialClassName);
Set<String> fullMatchingClassNames = Sets.newLinkedHashSet();
Set<String> matchingClassNames = Sets.newLinkedHashSet();
// also check loaded classes, e.g. for groovy classes
Iterator<String> i = classNameLocations.keySet().iterator();
if (instrumentation != null) {
List<String> loadedClassNames = Lists.newArrayList();
for (Class<?> clazz : instrumentation.getAllLoadedClasses()) {
if (!clazz.getName().startsWith("[")) {
loadedClassNames.add(clazz.getName());
}
}
i = Iterators.concat(i, loadedClassNames.iterator());
}
while (i.hasNext()) {
String className = i.next();
String classNameUpper = className.toUpperCase(Locale.ENGLISH);
boolean potentialFullMatch = matcher.isPotentialFullMatch(classNameUpper);
if (matchingClassNames.size() == limit && !potentialFullMatch) {
// once limit reached, only consider full matches
continue;
}
if (fullMatchingClassNames.size() == limit) {
break;
}
if (matcher.isPotentialMatch(classNameUpper)) {
if (potentialFullMatch) {
fullMatchingClassNames.add(className);
} else {
matchingClassNames.add(className);
}
}
}
return combineClassNamesWithLimit(fullMatchingClassNames, matchingClassNames, limit);
}
// using synchronization over concurrent structures in this cache to conserve memory
synchronized ImmutableList<UiAnalyzedMethod> getAnalyzedMethods(String className) {
// update cache before proceeding
updateCache();
Set<UiAnalyzedMethod> analyzedMethods = Sets.newHashSet();
Collection<Location> locations = classNameLocations.get(className);
for (Location location : locations) {
try {
analyzedMethods.addAll(getAnalyzedMethods(location, className));
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
}
if (instrumentation != null) {
// also check loaded classes, e.g. for groovy classes
for (Class<?> clazz : instrumentation.getAllLoadedClasses()) {
if (clazz.getName().equals(className)) {
analyzedMethods.addAll(getAnalyzedMethods(clazz));
}
}
}
return ImmutableList.copyOf(analyzedMethods);
}
// using synchronization over concurrent structures in this cache to conserve memory
synchronized void updateCache() {
Multimap<String, Location> newClassNameLocations = HashMultimap.create();
for (ClassLoader loader : getKnownClassLoaders()) {
updateCache(loader, newClassNameLocations);
}
updateCacheWithClasspathClasses(newClassNameLocations);
updateCacheWithBootstrapClasses(newClassNameLocations);
if (!newClassNameLocations.isEmpty()) {
// multimap that sorts keys and de-dups values while maintains value ordering
SetMultimap<String, Location> newMap =
MultimapBuilder.treeKeys().linkedHashSetValues().build();
newMap.putAll(classNameLocations);
newMap.putAll(newClassNameLocations);
classNameLocations = ImmutableMultimap.copyOf(newMap);
}
}
private ImmutableList<String> combineClassNamesWithLimit(Set<String> fullMatchingClassNames,
Set<String> matchingClassNames, int limit) {
if (fullMatchingClassNames.size() < limit) {
int space = limit - fullMatchingClassNames.size();
int numToAdd = Math.min(space, matchingClassNames.size());
fullMatchingClassNames
.addAll(ImmutableList.copyOf(Iterables.limit(matchingClassNames, numToAdd)));
}
return ImmutableList.copyOf(fullMatchingClassNames);
}
@GuardedBy("this")
private void updateCacheWithClasspathClasses(Multimap<String, Location> newClassNameLocations) {
String javaClassPath = StandardSystemProperty.JAVA_CLASS_PATH.value();
if (javaClassPath == null) {
return;
}
for (String path : Splitter.on(File.pathSeparatorChar).split(javaClassPath)) {
File file = new File(path);
Location location = getLocationFromFile(file);
if (location != null) {
loadClassNames(location, newClassNameLocations);
}
}
}
// TODO refactor this and above method which are nearly identical
@GuardedBy("this")
private void updateCacheWithBootstrapClasses(Multimap<String, Location> newClassNameLocations) {
String bootClassPath = System.getProperty("sun.boot.class.path");
if (bootClassPath == null) {
return;
}
for (String path : Splitter.on(File.pathSeparatorChar).split(bootClassPath)) {
File file = new File(path);
Location location = getLocationFromFile(file);
if (location != null) {
loadClassNames(location, newClassNameLocations);
}
}
}
private List<UiAnalyzedMethod> getAnalyzedMethods(Location location, String className)
throws IOException {
byte[] bytes = getBytes(location, className);
return getAnalyzedMethods(bytes);
}
private List<UiAnalyzedMethod> getAnalyzedMethods(byte[] bytes) throws IOException {
AnalyzingClassVisitor cv = new AnalyzingClassVisitor();
ClassReader cr = new ClassReader(bytes);
cr.accept(cv, 0);
return cv.getAnalyzedMethods();
}
private List<UiAnalyzedMethod> getAnalyzedMethods(Class<?> clazz) {
List<UiAnalyzedMethod> analyzedMethods = Lists.newArrayList();
for (Method method : clazz.getDeclaredMethods()) {
if (method.isSynthetic() || Modifier.isNative(method.getModifiers())) {
// don't add synthetic or native methods to the analyzed model
continue;
}
if (method.getName().startsWith("glowroot$")) {
// don't add glowroot mixin methods (this naming is just by convention)
continue;
}
ImmutableUiAnalyzedMethod.Builder builder = ImmutableUiAnalyzedMethod.builder();
builder.name(method.getName());
for (Class<?> parameterType : method.getParameterTypes()) {
// Class.getName() for arrays returns internal notation (e.g. "[B" for byte array)
// so using Type.getType().getClassName() instead
builder.addParameterTypes(Type.getType(parameterType).getClassName());
}
// Class.getName() for arrays returns internal notation (e.g. "[B" for byte array)
// so using Type.getType().getClassName() instead
builder.returnType(Type.getType(method.getReturnType()).getClassName());
builder.modifiers(method.getModifiers());
for (Class<?> exceptionType : method.getExceptionTypes()) {
builder.addExceptions(exceptionType.getName());
}
analyzedMethods.add(builder.build());
}
return analyzedMethods;
}
@GuardedBy("this")
private void updateCache(ClassLoader loader, Multimap<String, Location> newClassNameLocations) {
List<URL> urls = getURLs(loader);
List<Location> locations = Lists.newArrayList();
for (URL url : urls) {
Location location = tryToGetFileFromURL(url, loader);
if (location != null) {
locations.add(location);
}
}
for (Location location : locations) {
loadClassNames(location, newClassNameLocations);
}
}
private @Nullable Location tryToGetFileFromURL(URL url, ClassLoader loader) {
if (url.getProtocol().equals("vfs")) {
// special case for jboss/wildfly
try {
return getFileFromJBossVfsURL(url, loader);
} catch (Exception e) {
logger.warn(e.getMessage(), e);
}
return null;
}
try {
URI uri = url.toURI();
if (uri.getScheme().equals("file")) {
return getLocationFromFile(new File(uri));
} else if (uri.getScheme().equals("jar")) {
String f = uri.getSchemeSpecificPart();
if (f.startsWith("file:")) {
return getLocationFromJarFile(f);
}
}
} catch (URISyntaxException e) {
// log exception at debug level
logger.debug(e.getMessage(), e);
}
return null;
}
private List<URL> getURLs(ClassLoader loader) {
if (loader instanceof URLClassLoader) {
try {
return Lists.newArrayList(((URLClassLoader) loader).getURLs());
} catch (Exception e) {
// tomcat WebappClassLoader.getURLs() throws NullPointerException after stop() has
// been called on the WebappClassLoader (this happens, for example, after a webapp
// fails to load)
//
// log exception at debug level
logger.debug(e.getMessage(), e);
return ImmutableList.of();
}
}
// special case for jboss/wildfly
try {
return Collections.list(loader.getResources("/"));
} catch (IOException e) {
logger.warn(e.getMessage(), e);
return ImmutableList.of();
}
}
private List<ClassLoader> getKnownClassLoaders() {
List<ClassLoader> loaders = analyzedWorld.getClassLoaders();
if (loaders.isEmpty()) {
// this is needed for testing the UI outside of javaagent
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
if (systemClassLoader == null) {
return ImmutableList.of();
} else {
return ImmutableList.of(systemClassLoader);
}
}
return loaders;
}
private void loadClassNames(Location location,
Multimap<String, Location> newClassNameLocations) {
if (classpathLocations.contains(location)) {
return;
}
// add to classpath at top of method to avoid infinite recursion in case of cycle in
// Manifest Class-Path
classpathLocations.add(location);
try {
File dir = location.directory();
File jarFile = location.jarFile();
if (dir != null) {
loadClassNamesFromDirectory(dir, "", location, newClassNameLocations);
} else if (jarFile != null) {
String nestedJarFilePath = location.nestedJarFilePath();
if (nestedJarFilePath == null) {
loadClassNamesFromJarFile(jarFile, location, newClassNameLocations);
} else {
loadClassNamesFromNestedJarFile(jarFile, nestedJarFilePath, location,
newClassNameLocations);
}
} else {
throw new AssertionError("Both Location directory() and jarFile() are null");
}
} catch (IllegalArgumentException e) {
// File(URI) constructor can throw IllegalArgumentException
logger.debug(e.getMessage(), e);
} catch (IOException e) {
logger.debug("error reading classes from file: {}", location, e);
}
}
private static void loadClassNamesFromDirectory(File dir, String prefix, Location location,
Multimap<String, Location> newClassNameLocations) throws MalformedURLException {
File[] files = dir.listFiles();
if (files == null) {
return;
}
for (File file : files) {
String name = file.getName();
if (file.isFile() && name.endsWith(".class")) {
String className = prefix + name.substring(0, name.lastIndexOf('.'));
newClassNameLocations.put(className, location);
} else if (file.isDirectory()) {
loadClassNamesFromDirectory(file, prefix + name + ".", location,
newClassNameLocations);
}
}
}
private void loadClassNamesFromJarFile(File jarFile, Location location,
Multimap<String, Location> newClassNameLocations) throws IOException {
Closer closer = Closer.create();
InputStream s = new FileInputStream(jarFile);
JarInputStream jarIn = closer.register(new JarInputStream(s));
try {
loadClassNamesFromManifestClassPath(jarIn, jarFile, newClassNameLocations);
loadClassNamesFromJarInputStream(jarIn, location, newClassNameLocations);
} catch (Throwable t) {
throw closer.rethrow(t);
} finally {
closer.close();
}
}
private void loadClassNamesFromManifestClassPath(JarInputStream jarIn, File jarFile,
Multimap<String, Location> newClassNameLocations) {
Manifest manifest = jarIn.getManifest();
if (manifest == null) {
return;
}
String classpath = manifest.getMainAttributes().getValue("Class-Path");
if (classpath == null) {
return;
}
URI baseUri = jarFile.toURI();
for (String path : Splitter.on(' ').omitEmptyStrings().split(classpath)) {
File file = new File(baseUri.resolve(path));
Location location = getLocationFromFile(file);
if (location != null) {
loadClassNames(location, newClassNameLocations);
}
}
}
private static void loadClassNamesFromNestedJarFile(File jarFile, String nestedJarFilePath,
Location location, Multimap<String, Location> newClassNameLocations)
throws IOException {
URI uri;
try {
uri = new URI("jar", "file:" + jarFile.getPath() + "!/" + nestedJarFilePath, "");
} catch (URISyntaxException e) {
// this is a programmatic error
throw new RuntimeException(e);
}
Closer closer = Closer.create();
InputStream s = uri.toURL().openStream();
JarInputStream jarIn = closer.register(new JarInputStream(s));
try {
loadClassNamesFromJarInputStream(jarIn, location, newClassNameLocations);
} catch (Throwable t) {
throw closer.rethrow(t);
} finally {
closer.close();
}
}
private static void loadClassNamesFromJarInputStream(JarInputStream jarIn, Location location,
Multimap<String, Location> newClassNameLocations) throws IOException {
JarEntry jarEntry;
while ((jarEntry = jarIn.getNextJarEntry()) != null) {
if (jarEntry.isDirectory()) {
continue;
}
String name = jarEntry.getName();
if (!name.endsWith(".class")) {
continue;
}
String className = name.substring(0, name.lastIndexOf('.')).replace('/', '.');
newClassNameLocations.put(className, location);
}
}
private static @Nullable Location getFileFromJBossVfsURL(URL url, ClassLoader loader)
throws Exception {
Object virtualFile = url.openConnection().getContent();
Class<?> virtualFileClass = loader.loadClass("org.jboss.vfs.VirtualFile");
Method getPhysicalFileMethod = virtualFileClass.getMethod("getPhysicalFile");
Method getNameMethod = virtualFileClass.getMethod("getName");
File physicalFile = (File) getPhysicalFileMethod.invoke(virtualFile);
checkNotNull(physicalFile, "org.jboss.vfs.VirtualFile.getPhysicalFile() returned null");
String name = (String) getNameMethod.invoke(virtualFile);
checkNotNull(name, "org.jboss.vfs.VirtualFile.getName() returned null");
File file = new File(physicalFile.getParentFile(), name);
return getLocationFromFile(file);
}
@Value.Immutable(prehash = true)
interface UiAnalyzedMethod {
String name();
// these are class names
ImmutableList<String> parameterTypes();
String returnType();
int modifiers();
@Nullable
String signature();
ImmutableList<String> exceptions();
}
private static class PartialClassNameMatcher {
private final String partialClassNameUpper;
private final String prefixedPartialClassNameUpper1;
private final String prefixedPartialClassNameUpper2;
private PartialClassNameMatcher(String partialClassName) {
partialClassNameUpper = partialClassName.toUpperCase(Locale.ENGLISH);
prefixedPartialClassNameUpper1 = '.' + partialClassNameUpper;
prefixedPartialClassNameUpper2 = '$' + partialClassNameUpper;
}
private boolean isPotentialFullMatch(String classNameUpper) {
return classNameUpper.equals(partialClassNameUpper)
|| classNameUpper.endsWith(prefixedPartialClassNameUpper1)
|| classNameUpper.endsWith(prefixedPartialClassNameUpper2);
}
private boolean isPotentialMatch(String classNameUpper) {
return classNameUpper.startsWith(partialClassNameUpper)
|| classNameUpper.contains(prefixedPartialClassNameUpper1)
|| classNameUpper.contains(prefixedPartialClassNameUpper2);
}
}
private static class AnalyzingClassVisitor extends ClassVisitor {
private final List<UiAnalyzedMethod> analyzedMethods = Lists.newArrayList();
private AnalyzingClassVisitor() {
super(ASM5);
}
@Override
public @Nullable MethodVisitor visitMethod(int access, String name, String desc,
@Nullable String signature, String /*@Nullable*/[] exceptions) {
if ((access & ACC_SYNTHETIC) != 0 || (access & ACC_NATIVE) != 0) {
// don't add synthetic or native methods to the analyzed model
return null;
}
if (name.equals("<init>")) {
// don't add constructors to the analyzed model
return null;
}
ImmutableUiAnalyzedMethod.Builder builder = ImmutableUiAnalyzedMethod.builder();
builder.name(name);
for (Type parameterType : Type.getArgumentTypes(desc)) {
builder.addParameterTypes(parameterType.getClassName());
}
builder.returnType(Type.getReturnType(desc).getClassName());
builder.modifiers(access);
if (exceptions != null) {
for (String exception : exceptions) {
builder.addExceptions(ClassNames.fromInternalName(exception));
}
}
analyzedMethods.add(builder.build());
return null;
}
private List<UiAnalyzedMethod> getAnalyzedMethods() {
return analyzedMethods;
}
}
private static @Nullable Location getLocationFromFile(File file) {
boolean exists = file.exists();
if (exists && file.isDirectory()) {
return ImmutableLocation.builder().directory(file).build();
} else if (exists && file.getName().endsWith(".jar")) {
return ImmutableLocation.builder().jarFile(file).build();
} else {
return null;
}
}
private static Location getLocationFromJarFile(String f) {
int index = f.indexOf("!/");
File jarFile = new File(f.substring(5, index));
String nestedJarFilePath = f.substring(index + 2);
if (nestedJarFilePath.isEmpty()) {
// the jar file itself
return ImmutableLocation.builder().jarFile(jarFile).build();
}
// strip off trailing !/
nestedJarFilePath = nestedJarFilePath.substring(0, nestedJarFilePath.length() - 2);
return ImmutableLocation.builder()
.jarFile(jarFile)
.nestedJarFilePath(nestedJarFilePath)
.build();
}
private static byte[] getBytes(Location location, String className) throws IOException {
String name = className.replace('.', '/') + ".class";
File dir = location.directory();
File jarFile = location.jarFile();
if (dir != null) {
URI uri = new File(dir, name).toURI();
return Resources.toByteArray(uri.toURL());
} else if (jarFile != null) {
String nestedJarFilePath = location.nestedJarFilePath();
if (nestedJarFilePath == null) {
return getBytesFromJarFile(name, jarFile);
} else {
return getBytesFromNestedJarFile(name, jarFile, nestedJarFilePath);
}
} else {
throw new AssertionError("Both Location directory() and jarFile() are null");
}
}
private static byte[] getBytesFromJarFile(String name, File jarFile) throws IOException {
String path = jarFile.getPath();
URI uri;
try {
uri = new URI("jar", "file:" + path + "!/" + name, "");
} catch (URISyntaxException e) {
// this is a programmatic error
throw new RuntimeException(e);
}
return Resources.toByteArray(uri.toURL());
}
private static byte[] getBytesFromNestedJarFile(String name, File jarFile,
String nestedJarFilePath) throws IOException {
String path = jarFile.getPath();
URI uri;
try {
uri = new URI("jar", "file:" + path + "!/" + nestedJarFilePath, "");
} catch (URISyntaxException e) {
// this is a programmatic error
throw new RuntimeException(e);
}
Closer closer = Closer.create();
InputStream s = uri.toURL().openStream();
JarInputStream jarIn = closer.register(new JarInputStream(s));
try {
JarEntry jarEntry;
while ((jarEntry = jarIn.getNextJarEntry()) != null) {
if (jarEntry.isDirectory()) {
continue;
}
if (jarEntry.getName().equals(name)) {
return ByteStreams.toByteArray(jarIn);
}
}
} catch (Throwable t) {
throw closer.rethrow(t);
} finally {
closer.close();
}
throw new UnsupportedOperationException();
}
@Value.Immutable
interface Location {
@Nullable
File directory();
@Nullable
File jarFile();
@Nullable
String nestedJarFilePath();
}
}