com.google.code.siren4j.util.ReflectionUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.google.code.siren4j.util.ReflectionUtils.java

Source

/*******************************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2013 Erik R Serating
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
 * persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *********************************************************************************************/
package com.google.code.siren4j.util;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;

import com.google.code.siren4j.annotations.Siren4JProperty;
import com.google.code.siren4j.annotations.Siren4JPropertyIgnore;
import com.google.code.siren4j.annotations.Siren4JSubEntity;
import com.google.code.siren4j.converter.ReflectedInfo;
import com.google.code.siren4j.error.Siren4JException;
import com.google.code.siren4j.error.Siren4JRuntimeException;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

public class ReflectionUtils {

    public static final String GETTER_PREFIX = "get";
    public static final String GETTER_PREFIX_BOOLEAN = "is";
    public static final String SETTER_PREFIX = "set";
    /**
     * Field info is cached as it is an expensive operation but the values don't actually change
     * until code is changed and recompiled.
     */
    public static Cache<Class<?>, List<ReflectedInfo>> fieldInfoCache = CacheBuilder.newBuilder().maximumSize(2000)
            .build();
    /**
     * Find Method cache another expensive operation with reflection so let's cache it.
     */
    public static Cache<String, Method> findMethodCache = CacheBuilder.newBuilder().maximumSize(2000).build();

    /**
     * List of types considered as Siren properties, generally simple types.
     */
    public static final Class<?>[] propertyTypes = new Class<?>[] { int.class, Integer.class, long.class,
            Long.class, double.class, Double.class, float.class, Float.class, short.class, Short.class, byte.class,
            Byte.class, boolean.class, Boolean.class, String.class, Date.class, int[].class, Integer[].class,
            long[].class, Long[].class, double[].class, Double[].class, float[].class, Float[].class, short[].class,
            Short[].class, byte[].class, Byte[].class, boolean[].class, Boolean[].class, String[].class,
            Date[].class, BigDecimal.class, BigInteger.class };

    private ReflectionUtils() {

    }

    /**
     * Remove the getter prefix ('is', 'get') from the the method name passed in.
     *
     * @param name the method name.
     * @return the method name without the prefix, may be <code>null</code> or empty if the name passed in was that way.
     */
    public static String stripGetterPrefix(String name) {
        if (StringUtils.isBlank(name)) {
            return name; // This should never happen.
        }
        String[] mName = StringUtils.splitByCharacterTypeCamelCase(name);
        StringBuilder sb = new StringBuilder();
        for (int i = 1; i < mName.length; i++) {
            sb.append(mName[i]);
        }
        return StringUtils.uncapitalize(sb.toString());
    }

    /**
     * Find the field for the getter method based on the get methods name. It finds the field on the declaring class.
     *
     * @param method cannot be <code>null</code>.
     * @return the field or <code>null</code> if not found.
     */
    public static Field getGetterField(Method method) {
        if (method == null) {
            throw new IllegalArgumentException("method cannot be null.");
        }
        Class<?> clazz = method.getDeclaringClass();
        String fName = stripGetterPrefix(method.getName());
        Field field = null;
        try {
            field = findField(clazz, fName);
        } catch (Exception ignore) {
        }
        return field;

    }

    /**
     * Retrieve all fields deemed as Exposed, i.e. they are public or have a public accessor method or are marked by an
     * annotation to be exposed.
     *
     * @param clazz cannot be <code>null</code>.
     * @return
     */
    public static List<ReflectedInfo> getExposedFieldInfo(final Class<?> clazz) {
        List<ReflectedInfo> results = null;
        try {
            results = fieldInfoCache.get(clazz, new Callable<List<ReflectedInfo>>() {
                /**
                 * This method is called if the value is not found in the cache. This
                 * is where the real reflection work is done.
                 */
                public List<ReflectedInfo> call() throws Exception {
                    List<ReflectedInfo> exposed = new ArrayList<ReflectedInfo>();
                    for (Method m : clazz.getMethods()) {
                        if (ReflectionUtils.isGetter(m) && !isIgnored(m)) {
                            Field f = getGetterField(m);
                            if (f != null && !isIgnored(f)) {
                                f.setAccessible(true);
                                Siren4JProperty propAnno = f.getAnnotation(Siren4JProperty.class);
                                String effectiveName = propAnno != null
                                        ? StringUtils.defaultIfEmpty(propAnno.name(), f.getName())
                                        : f.getName();
                                Siren4JSubEntity subAnno = f.getAnnotation(Siren4JSubEntity.class);
                                if (subAnno != null && !ArrayUtils.isEmpty(subAnno.rel())) {
                                    effectiveName = subAnno.rel().length == 1 ? subAnno.rel()[0]
                                            : ArrayUtils.toString(subAnno.rel());
                                }
                                exposed.add(new ReflectedInfo(f, m, ReflectionUtils.getSetter(clazz, f),
                                        effectiveName));
                            }
                        }
                    }
                    return exposed;
                }

            });
        } catch (ExecutionException e) {
            throw new Siren4JRuntimeException(e);
        }
        return results;

    }

    /**
     * Retrieve the setter for the specified class/field if it exists.
     *
     * @param clazz
     * @param f
     * @return
     */
    public static Method getSetter(Class<?> clazz, Field f) {
        Method setter = null;
        for (Method m : clazz.getMethods()) {
            if (ReflectionUtils.isSetter(m)
                    && m.getName().equals(SETTER_PREFIX + StringUtils.capitalize(f.getName()))) {
                setter = m;
                break;
            }
        }
        return setter;
    }

    /**
     * Determine if the method is a getter.
     *
     * @param method
     * @return
     */
    public static boolean isGetter(Method method) {
        String name = method.getName();
        String[] splitname = StringUtils.splitByCharacterTypeCamelCase(name);
        return !method.getReturnType().equals(void.class) && Modifier.isPublic(method.getModifiers())
                && (splitname[0].equals(GETTER_PREFIX_BOOLEAN) || splitname[0].equals(GETTER_PREFIX));
    }

    /**
     * Determine if the method is a setter.
     *
     * @param method
     * @return
     */
    public static boolean isSetter(Method method) {
        String name = method.getName();
        String[] splitname = StringUtils.splitByCharacterTypeCamelCase(name);
        return method.getReturnType().equals(void.class) && Modifier.isPublic(method.getModifiers())
                && splitname[0].equals(SETTER_PREFIX);
    }

    /**
     * Replaces field tokens with the actual value in the fields. The field types must be simple property types
     *
     * @param str
     * @param fields info
     * @param parentMode
     * @param obj
     * @return
     * @throws Siren4JException
     */
    public static String replaceFieldTokens(Object obj, String str, List<ReflectedInfo> fields, boolean parentMode)
            throws Siren4JException {
        Map<String, Field> index = new HashMap<String, Field>();
        if (StringUtils.isBlank(str)) {
            return str;
        }
        if (fields != null) {
            for (ReflectedInfo info : fields) {
                Field f = info.getField();
                if (f != null) {
                    index.put(f.getName(), f);
                }
            }
        }
        try {
            for (String key : ReflectionUtils.getTokenKeys(str)) {
                if ((!parentMode && !key.startsWith("parent.")) || (parentMode && key.startsWith("parent."))) {
                    String fieldname = key.startsWith("parent.") ? key.substring(7) : key;
                    if (index.containsKey(fieldname)) {
                        Field f = index.get(fieldname);
                        if (f.getType().isEnum() || ArrayUtils.contains(propertyTypes, f.getType())) {
                            String replacement = "";
                            Object theObject = f.get(obj);
                            if (f.getType().isEnum()) {
                                replacement = theObject == null ? "" : ((Enum) theObject).name();
                            } else {
                                replacement = theObject == null ? "" : theObject.toString();
                            }
                            str = str.replaceAll("\\{" + key + "\\}", Matcher.quoteReplacement("" + replacement));
                        }
                    }
                }
            }
        } catch (Exception e) {
            throw new Siren4JException(e);
        }
        return str;

    }

    /**
     * Removes the square brackets that signify reserved from inside the tokens in the string.
     *
     * @param str
     * @return
     */
    public static String flattenReservedTokens(String str) {
        if (StringUtils.isBlank(str)) {
            return str;
        }
        return str.replaceAll("\\{\\[", "{").replaceAll("\\]\\}", "}");
    }

    /**
     * Determine if the field or method is ignored because the <code>SirenPropertyIgnore</code> annotation is present.
     *
     * @param obj
     * @return
     */
    public static boolean isIgnored(AccessibleObject obj) {
        return obj.isAnnotationPresent(Siren4JPropertyIgnore.class);
    }

    /**
     * Retrieve all token keys from a string. A token is found by its its start and end delimiters which are open and close
     * curly braces.
     *
     * @param str the string to parse, may be <code>null</code> or empty.
     * @return the set of unique token keys. Never <code>null</code>.May be empty.
     */
    public static Set<String> getTokenKeys(String str) {

        Set<String> results = new HashSet<String>();
        String sDelim = "{";
        String eDelim = "}";
        if (StringUtils.isBlank(str)) {
            return results;
        }
        int start = -1;
        int end = 0;

        do {
            start = str.indexOf(sDelim, end);
            if (start != -1) {
                end = str.indexOf(eDelim, start);
                if (end != -1) {
                    results.add(str.substring(start + sDelim.length(), end));
                }
            }
        } while (start != -1 && end != -1);
        return results;
    }

    /**
     * Convenience method to retrieve the field value for the specified object wrapped to
     * catch exceptions and re throw as <code>Siren4JRuntimeException</code>.
     *
     * @param field cannot be <code>null</code>.
     * @param obj may be <code>null</code>.
     * @return the value, may be <code>null</code>.
     */
    public static Object getFieldValue(Field field, Object obj) {
        if (field == null) {
            throw new IllegalArgumentException("field cannot be null.");
        }
        try {
            return field.get(obj);
        } catch (IllegalArgumentException e) {
            throw new Siren4JRuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new Siren4JRuntimeException(e);
        }
    }

    /**
     * @param clazz
     * @param methodName
     * @param parameterTypes
     * @return
     * @throws NoSuchMethodException
     */
    public static Method findMethod(final Class<?> clazz, final String methodName, final Class<?>[] parameterTypes)
            throws NoSuchMethodException {
        String key = makeFindMethodCacheKey(clazz, methodName, parameterTypes);
        Method method = null;
        try {
            method = findMethodCache.get(key, new Callable<Method>() {

                public Method call() throws Exception {
                    return _findMethod(clazz, methodName, parameterTypes);
                }

            });
        } catch (ExecutionException e) {
            throw new Siren4JRuntimeException(e);
        }
        return method;
    }

    private static Method _findMethod(Class<?> clazz, String methodName, Class<?>[] parameterTypes)
            throws NoSuchMethodException {
        Method method = null;
        try {
            method = clazz.getMethod(methodName, parameterTypes);
        } catch (NoSuchMethodException e) {
            try {
                method = clazz.getDeclaredMethod(methodName, parameterTypes);
            } catch (NoSuchMethodException ignore) {
            }
        }
        if (method == null) {
            Class<?> superClazz = clazz.getSuperclass();
            if (superClazz != null) {
                method = _findMethod(superClazz, methodName, parameterTypes);
            }
            if (method == null) {
                throw new NoSuchMethodException("Method: " + methodName);
            } else {
                return method;
            }
        } else {
            return method;
        }
    }

    public static Field findField(Class<?> clazz, String fieldName) throws NoSuchFieldException {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
        } catch (NoSuchFieldException e) {

        }
        if (field == null) {
            Class<?> superClazz = clazz.getSuperclass();
            if (superClazz != null) {
                field = findField(superClazz, fieldName);
            }
            if (field == null) {
                throw new NoSuchFieldException("Field: " + fieldName);
            } else {
                return field;
            }
        } else {
            return field;
        }
    }

    private static String makeFindMethodCacheKey(Class<?> clazz, String methodName, Class<?>[] parameterTypes) {
        StringBuilder key = new StringBuilder();
        key.append(clazz.getName());
        key.append("_");
        key.append(methodName);

        if (parameterTypes != null) {
            for (Class<?> c : parameterTypes) {
                key.append("_");
                key.append(c.getName());
            }
        }
        return key.toString();
    }

    /**
     * Helper method to find the field info by its effective name from the passed in list of info.
     *
     * @param infoList cannot be <code>null</code>.
     * @param name cannot be <code>null</code> or empty.
     * @return the info or <code>null</code> if not found.
     */
    public static ReflectedInfo getFieldInfoByEffectiveName(List<ReflectedInfo> infoList, String name) {
        if (infoList == null) {
            throw new IllegalArgumentException("infoList cannot be null.");
        }
        if (StringUtils.isBlank(name)) {
            throw new IllegalArgumentException("name cannot be null or empty.");
        }
        ReflectedInfo result = null;
        for (ReflectedInfo info : infoList) {
            if (name.equals(info.getEffectiveName())) {
                result = info;
                break;
            }
        }
        return result;
    }

    /**
     * Helper method to find the field info by its name from the passed in list of info.
     *
     * @param infoList cannot be <code>null</code>.
     * @param name cannot be <code>null</code> or empty.
     * @return the info or <code>null</code> if not found.
     */
    public static ReflectedInfo getFieldInfoByName(List<ReflectedInfo> infoList, String name) {
        if (infoList == null) {
            throw new IllegalArgumentException("infoList cannot be null.");
        }
        if (StringUtils.isBlank(name)) {
            throw new IllegalArgumentException("name cannot be null or empty.");
        }
        ReflectedInfo result = null;
        for (ReflectedInfo info : infoList) {
            if (info.getField() == null) {
                continue; //should never happen.
            }
            if (name.equals(info.getField().getName())) {
                result = info;
                break;
            }
        }
        return result;
    }

    /**
     * Sets the fields value, first by attempting to call the setter method if it exists and then
     * falling back to setting the field directly.
     *
     * @param obj the object instance to set the value on, cannot be <code>null</code>.
     * @param info the fields reflected info object, cannot be <code>null</code>.
     * @param value the value to set on the field, may be <code>null</code>.
     * @throws Siren4JException upon reflection error.
     */
    public static void setFieldValue(Object obj, ReflectedInfo info, Object value) throws Siren4JException {
        if (obj == null) {
            throw new IllegalArgumentException("obj cannot be null");
        }
        if (info == null) {
            throw new IllegalArgumentException("info cannot be null");
        }
        if (info.getSetter() != null) {
            Method setter = info.getSetter();
            setter.setAccessible(true);
            try {
                setter.invoke(obj, new Object[] { value });
            } catch (Exception e) {
                throw new Siren4JException(e);
            }
        } else {
            // No setter set field directly
            try {
                info.getField().set(obj, value);
            } catch (Exception e) {
                throw new Siren4JException(e);
            }
        }

    }

    @SuppressWarnings("rawtypes")
    public static boolean isSirenProperty(Class<?> type, Object obj, Field field) {
        boolean isProp = false;

        Siren4JProperty anno = field.getAnnotation(Siren4JProperty.class);
        if (anno != null || type.isEnum()) {
            isProp = true;
        } else if (ArrayUtils.contains(propertyTypes, type)) {
            isProp = true;
        } else if (obj != null && Collection.class.isAssignableFrom(type)) {
            //Try to determine value type
            if (!((Collection) obj).isEmpty()) {
                Object first = findFirstNonNull(((Collection) obj).iterator());
                if (first == null || ArrayUtils.contains(propertyTypes, first.getClass())) {
                    isProp = true;
                }
            }
        } else if (obj != null && Map.class.isAssignableFrom(type)) {
            //Try to determine value types of key and value
            if (!((Map) obj).isEmpty()) {
                Object firstKey = findFirstNonNull(((Map) obj).keySet().iterator());
                Object firstVal = findFirstNonNull(((Map) obj).entrySet().iterator());
                if ((firstKey == null || ArrayUtils.contains(propertyTypes, firstKey.getClass()))
                        && (firstVal == null || ArrayUtils.contains(propertyTypes,
                                ((HashMap.Entry) firstVal).getValue().getClass()))) {
                    isProp = true;
                }
            }
        }
        return isProp;
    }

    /**
     * Finds first non <code>null</code> value in an iterator, does return <code>null</code> if
     * no non <code>null</code> values are found.
     * @param it
     * @return
     */
    @SuppressWarnings("rawtypes")
    private static Object findFirstNonNull(Iterator it) {
        Object result = null;
        while (it.hasNext()) {
            result = it.next();
            if (result != null) {
                break;
            }
        }
        return result;
    }

}