org.glowroot.agent.plugin.servlet.Beans.java Source code

Java tutorial

Introduction

Here is the source code for org.glowroot.agent.plugin.servlet.Beans.java

Source

/*
 * 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);
        }
    }
}