/*
* Copyright 2012-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.plugin.servlet;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentMap;
import javax.annotation.Nullable;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Maps;
import org.glowroot.agent.plugin.api.Agent;
import org.glowroot.agent.plugin.api.Logger;
class Beans {
private static final Logger logger = Agent.getLogger(Beans.class);
// sentinel method is used to represent null value in the weak valued ConcurrentMap below
// using guava's Optional would make the weakness on the Optional instance instead of on the
// Method instance which would cause unnecessary clearing of the map values
private static final Method SENTINEL_METHOD;
static {
try {
SENTINEL_METHOD = Beans.class.getDeclaredMethod("sentinelMethod");
} catch (Exception e) {
// unrecoverable error
throw new AssertionError(e);
}
}
// note, not using nested loading cache since the nested loading cache maintains a strong
// reference to the class loader
//
// weak keys in loading cache to prevent Class retention
private static final LoadingCache<Class<?>, ConcurrentMap<String, AccessibleObject>> getters =
CacheBuilder.newBuilder()
.weakKeys()
.build(new CacheLoader<Class<?>, ConcurrentMap<String, AccessibleObject>>() {
@Override
public ConcurrentMap<String, AccessibleObject> load(Class<?> clazz) {
// weak values since Method has a strong reference to its Class which
// is used as the key in the outer loading cache
return new MapMaker().weakValues().makeMap();
}
});
// all getters for an individual class are only needed to handle wildcards at the end of a
// session attribute path, e.g. "user.*"
private static final LoadingCache<Class<?>, ImmutableMap<String, Method>> wildcardGetters =
CacheBuilder.newBuilder().weakKeys().build(new WildcardGettersCacheLoader());
private Beans() {}
static @Nullable Object value(@Nullable Object obj, String path) {
try {
return valueInternal(obj, path);
} catch (Exception e) {
// log exception at debug level
logger.debug(e.getMessage(), e);
return "<could not access>";
}
}
static Map<String, String> propertiesAsText(Object obj) {
Map<String, String> properties = Maps.newHashMap();
ImmutableMap<String, Method> allGettersForObj =
wildcardGetters.getUnchecked(obj.getClass());
for (Entry<String, Method> entry : allGettersForObj.entrySet()) {
try {
Object value = entry.getValue().invoke(obj);
if (value != null) {
properties.put(entry.getKey(), value.toString());
}
} catch (Exception e) {
// log exception at debug level
logger.debug(e.getMessage(), e);
properties.put(entry.getKey(), "<could not access>");
}
}
return properties;
}
private static @Nullable Object valueInternal(@Nullable Object obj, String path)
throws Exception {
if (obj == null) {
return null;
}
if (path.isEmpty()) {
return obj;
}
int index = path.indexOf('.');
String curr;
String remaining;
if (index == -1) {
curr = path;
remaining = "";
} else {
curr = path.substring(0, index);
remaining = path.substring(index + 1);
}
if (obj instanceof Map) {
return valueInternal(((Map<?, ?>) obj).get(curr), remaining);
}
AccessibleObject accessor = getAccessor(obj.getClass(), curr);
if (accessor.equals(SENTINEL_METHOD)) {
// no appropriate method found, dynamic paths that may or may not resolve
// correctly are ok, just return null
return null;
}
Object currItem = invoke(accessor, obj);
return valueInternal(currItem, remaining);
}
private static AccessibleObject getAccessor(Class<?> clazz, String name) {
ConcurrentMap<String, AccessibleObject> accessorsForType = getters.getUnchecked(clazz);
AccessibleObject accessor = accessorsForType.get(name);
if (accessor == null) {
accessor = loadAccessor(clazz, name);
accessor.setAccessible(true);
accessorsForType.put(name, accessor);
}
return accessor;
}
private static AccessibleObject loadAccessor(Class<?> clazz, String name) {
String capitalizedName = Character.toUpperCase(name.charAt(0)) + name.substring(1);
try {
return getMethod(clazz, "get" + capitalizedName);
} catch (Exception e) {
// log exception at trace level
logger.trace(e.getMessage(), e);
}
// fall back to "is" prefix
try {
return getMethod(clazz, "is" + capitalizedName);
} catch (Exception f) {
// log exception at trace level
logger.trace(f.getMessage(), f);
}
// fall back to no prefix
try {
return getMethod(clazz, name);
} catch (Exception g) {
// log exception at trace level
logger.trace(g.getMessage(), g);
}
// fall back to field access
try {
return getField(clazz, name);
} catch (Exception h) {
// log exception at trace level
logger.trace(h.getMessage(), h);
}
// log general failure message at debug level
logger.debug("no accessor found for {} in class {}", name, clazz.getName());
return SENTINEL_METHOD;
}
private static @Nullable Object invoke(AccessibleObject accessor, Object obj) throws Exception {
if (accessor instanceof Method) {
return ((Method) accessor).invoke(obj);
} else {
return ((Field) accessor).get(obj);
}
}
private static Method getMethod(Class<?> clazz, String methodName) throws Exception {
try {
return clazz.getMethod(methodName);
} catch (NoSuchMethodException e) {
// log exception at trace level
logger.trace(e.getMessage(), e);
return clazz.getDeclaredMethod(methodName);
}
}
private static Field getField(Class<?> clazz, String fieldName) throws Exception {
try {
return clazz.getField(fieldName);
} catch (NoSuchFieldException e) {
// log exception at trace level
logger.trace(e.getMessage(), e);
return clazz.getDeclaredField(fieldName);
}
}
// this unused private method is required for use as SENTINEL_METHOD above
@SuppressWarnings("unused")
private static void sentinelMethod() {}
private static class WildcardGettersCacheLoader
extends CacheLoader<Class<?>, ImmutableMap<String, Method>> {
@Override
public ImmutableMap<String, Method> load(Class<?> clazz) {
Map<String, Method> propertyNames = Maps.newHashMap();
for (Method method : clazz.getMethods()) {
String propertyName = getPropertyName(method);
if (propertyName == null) {
continue;
}
Method otherMethod = propertyNames.get(propertyName);
if (otherMethod != null && otherMethod.getName().startsWith("get")) {
// "getX" takes precedence over "isX"
continue;
}
propertyNames.put(propertyName, method);
}
return ImmutableMap.copyOf(propertyNames);
}
private static @Nullable String getPropertyName(Method method) {
if (method.getParameterTypes().length > 0) {
return null;
}
String methodName = method.getName();
if (methodName.equals("getClass")) {
// ignore this "getter"
return null;
}
if (startsWithAndThenUpperCaseChar(methodName, "get")) {
return getRemainingWithFirstCharLowercased(methodName, "get");
}
if (startsWithAndThenUpperCaseChar(methodName, "is")) {
return getRemainingWithFirstCharLowercased(methodName, "is");
}
return null;
}
private static boolean startsWithAndThenUpperCaseChar(String str, String prefix) {
return str.startsWith(prefix) && str.length() > prefix.length()
&& Character.isUpperCase(str.charAt(prefix.length()));
}
private static String getRemainingWithFirstCharLowercased(String str, String prefix) {
return Character.toLowerCase(str.charAt(prefix.length()))
+ str.substring(prefix.length() + 1);
}
}
}