/* * Copyright 2013, Arondor * * 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 com.arondor.common.reflection.parser.java; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.log4j.Logger; import com.arondor.common.management.mbean.annotation.Description; import com.arondor.common.management.mbean.annotation.Mandatory; import com.arondor.common.reflection.api.parser.AccessibleClassParser; import com.arondor.common.reflection.bean.java.AccessibleClassBean; import com.arondor.common.reflection.bean.java.AccessibleConstructorBean; import com.arondor.common.reflection.bean.java.AccessibleFieldBean; import com.arondor.common.reflection.bean.java.AccessibleMethodBean; import com.arondor.common.reflection.model.java.AccessibleClass; import com.arondor.common.reflection.model.java.AccessibleConstructor; import com.arondor.common.reflection.model.java.AccessibleField; import com.arondor.common.reflection.model.java.AccessibleMethod; import com.arondor.common.reflection.util.PrimitiveTypeUtil; public class JavaAccessibleClassParser implements AccessibleClassParser { private static final Logger LOG = Logger.getLogger(JavaAccessibleClassParser.class); /** * Expensive log for this class */ private static final boolean DEBUG = LOG.isDebugEnabled(); private boolean replaceDollarByPointForEmbeddedClasses = false; private boolean tryInstantiateClassForDefaultValue = false; /** * Convert a getter method name to an attribute name, in Java naming * conventions * * @param getterName * the getField() (or setField()) method * @return the field name, with first character in lowercase */ public String getterToAttribute(String getterName) { int offset = -1; if (getterName.startsWith("get") || getterName.startsWith("set")) { offset = 3; } else if (getterName.startsWith("is")) { offset = 2; } else { throw new IllegalArgumentException("Invalid call to getterToAttribute('" + getterName + "')"); } if (getterName.length() == offset) { return "none"; } String rName = getterName.substring(offset, offset + 1); String rgName = getterName.substring(offset + 1); return rName.toLowerCase() + rgName; } /** * Generate an attribute getter name * * @param name * the field name * @return the getter method name, getField() */ public String attributeToGetter(String name) { return "get" + name.substring(0, 1).toUpperCase() + name.substring(1); } /** * Generate an attribute getter name * * @param name * the field name * @return the getter method name, getField() */ public String booleanAttributeToGetter(String name) { return "is" + name.substring(0, 1).toUpperCase() + name.substring(1); } /** * Generate an attribute setter name * * @param name * the field name * @return the setter method name, setField() */ public String attributeToSetter(String name) { return "set" + name.substring(0, 1).toUpperCase() + name.substring(1); } /** * Check if a class is part of classes considered to be directly exposable * to JConsole * * @param clazz * the class to check * @return true if this class is considered exposable, false otherwise */ public boolean isExposableType(Class<?> clazz, boolean includeNonPrimitive) { if (PrimitiveTypeUtil.isPrimitiveType(clazz)) { return true; } if (clazz.getPackage() != null) { if (clazz.getPackage().getName().startsWith("javax.management")) { return false; } } return includeNonPrimitive; } /** * Check if an array of classes is exposable to JConsole * * @param parameterTypes * array of classes to check * @return true if all classes are exposable, false if at least one is not * exposable */ public boolean isExposableSignature(Class<?>[] parameterTypes, boolean includeNonPrimitive) { if (parameterTypes == null || parameterTypes.length == 0) { return true; } for (int i = 0; i < parameterTypes.length; i++) { if (!isExposableType(parameterTypes[i], includeNonPrimitive)) { return false; } } return true; } /** * Checks if a class is a 'void' type * * @param clazz * the class to check * @return true if the class is void, true otherwise */ public boolean isVoid(Class<?> clazz) { return (clazz.isPrimitive() && clazz.getName().equals("void")); } /** * Parse method signatures and put it in exposed attributes or exposed * methods * * @param clazz * the class being parsed * @param methods * the array of methods to parse * @param exposedAttributes * map of exposed attributes * @param exposedMethods * list of exposed methods */ private void parseExposedMethodAndAttributes(Class<?> clazz, Method[] methods, Map<String, AccessibleField> exposedAttributes, List<Method> exposedMethods, boolean includeNonPrimitive) { for (int mth = 0; mth < methods.length; mth++) { Method method = methods[mth]; if (DEBUG) { LOG.debug("Method : " + method.getName() + ", return=" + method.getReturnType().getName() + " (exposable=" + isExposableType(method.getReturnType(), includeNonPrimitive) + ")" + ", args=" + method.getParameterTypes().length + ", declaringClass=" + method.getDeclaringClass().getName()); } if (isIgnoredMethod(method)) { continue; } parseExposedMethod(clazz, exposedAttributes, exposedMethods, includeNonPrimitive, method); } } private void parseExposedMethod(Class<?> clazz, Map<String, AccessibleField> exposedAttributes, List<Method> exposedMethods, boolean includeNonPrimitive, Method method) { if (isGetterMethod(includeNonPrimitive, method)) { String prop = getterToAttribute(method.getName()); Class<?> fieldClass = method.getReturnType(); AccessibleFieldBean field = getBeanFromMethod(clazz, exposedAttributes, fieldClass, prop); field.setReadable(); setFieldDeclaredInClass(field, method.getDeclaringClass()); if (method.getName().startsWith("is")) { field.setIs(true); } } else if (isSetterMethod(includeNonPrimitive, method)) { String prop = getterToAttribute(method.getName()); Class<?> fieldType = method.getParameterTypes()[0]; AccessibleFieldBean field = getBeanFromMethod(clazz, exposedAttributes, fieldType, prop); field.setWritable(); setFieldDeclaredInClass(field, method.getDeclaringClass()); if (!field.getClassName().equals(fieldType.getName())) { LOG.warn("Incompatible setter type at class " + clazz.getName() + ", getter said " + field.getClassName() + ", setter said " + fieldType.getName()); // LOG.warn("Overriding getter type to the setter type " + // fieldType.getName()); field.setClassName(fieldType.getName()); } if (method.getGenericParameterTypes() != null && method.getGenericParameterTypes().length == 1) { addGenericParameter(field, method.getGenericParameterTypes()[0]); } } else if ((isVoid(method.getReturnType()) || (isExposableType(method.getReturnType(), includeNonPrimitive))) && (isExposableSignature(method.getParameterTypes(), includeNonPrimitive))) { exposedMethods.add(method); } else { if (DEBUG) { LOG.debug("Skipping method :" + method.getName() + ", modifiers=" + Modifier.toString(method.getModifiers())); } } } private void setFieldDeclaredInClass(AccessibleFieldBean field, Class<?> declaredClass) { if (field.getDeclaredInClass() != null) { if (field.getDeclaredInClass().equals(declaredClass.getName())) { return; } else { LOG.warn("Field " + field.getName() + " already declared in class " + field.getDeclaredInClass() + ", now said to be declared (or overridden) in " + declaredClass.getName()); return; } } field.setDeclaredInClass(declaredClass.getName()); } private void addGenericParameter(AccessibleFieldBean attributeInfo, Type type) { LOG.debug("Setting field " + attributeInfo.getName() + " : type=" + type); List<String> genericTypes = new ArrayList<String>(); if (type instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) type; for (Type subType : parameterizedType.getActualTypeArguments()) { LOG.debug("subType : " + subType); if (subType instanceof Class<?>) { Class<?> subTypeClass = (Class<?>) subType; genericTypes.add(subTypeClass.getName()); } else { return; } } } LOG.debug("Setting field " + attributeInfo.getName() + " : genericTypes=" + genericTypes); attributeInfo.setGenericParameterClassList(genericTypes); } private static final String[] IGNORED_METHODS = { "wait", "notifyAll", "notify", "finalize", "getClass", "equals", "toString", "hashCode" }; private boolean isIgnoredMethod(Method method) { if (!Modifier.isPublic(method.getModifiers()) || Modifier.isStatic(method.getModifiers())) { return true; } for (String mth : IGNORED_METHODS) { if (method.getName().equals(mth)) { return true; } } return false; } /** * Check whether this mehod is a setter method * * @param includeNonPrimitive * @param method * @return */ private boolean isSetterMethod(boolean includeNonPrimitive, Method method) { return method.getName().startsWith("set") && method.getParameterTypes().length == 1 && (isExposableSignature(method.getParameterTypes(), includeNonPrimitive)) && isVoid(method.getReturnType()); } /** * Check whether this mehod is a getter method * * @param includeNonPrimitive * @param method * @return */ private boolean isGetterMethod(boolean includeNonPrimitive, Method method) { return (method.getName().startsWith("get") || method.getName().startsWith("is")) && (isExposableType(method.getReturnType(), includeNonPrimitive)) && method.getParameterTypes().length == 0; } private AccessibleFieldBean getBeanFromMethod(Class<?> clazz, Map<String, AccessibleField> exposedAttributes, Class<?> propertyClass, String propertyName) { AccessibleFieldBean attributeInfo = (AccessibleFieldBean) exposedAttributes.get(propertyName); if (attributeInfo == null) { attributeInfo = new AccessibleFieldBean(propertyName, propertyName, propertyClass, true, false); exposedAttributes.put(propertyName, attributeInfo); if (DEBUG) { LOG.debug("New exposed attribute (R) : " + attributeInfo.getName()); } } else { if (!propertyClass.getName().equals(attributeInfo.getClassName())) { LOG.warn("Diverging classes at setter for class " + clazz.getName() + ", property " + propertyName + " : was " + attributeInfo.getClassName() + ", now is " + propertyClass.getName()); } } return attributeInfo; } private String getClassDescription(Class<?> clazz) { Description descAnnotation = clazz.getAnnotation(Description.class); if (descAnnotation != null) { return descAnnotation.value(); } return null; } private String getFieldDescription(Field field) { Description descAnnotation = field.getAnnotation(Description.class); if (descAnnotation != null) { return descAnnotation.value(); } return null; } private boolean getFieldMandatory(Field field) { Mandatory mandatoryAnnotation = field.getAnnotation(Mandatory.class); if (mandatoryAnnotation != null) { return mandatoryAnnotation.isMandatory(); } return false; } private List<String> getFieldEnumValue(Field field) { Class<?> fieldClass = field.getType(); List<String> enumNames = new ArrayList<String>(); if (fieldClass.isEnum()) { LOG.debug("Field " + field.getName() + " is an enum type of class=" + fieldClass.getName()); Enum<?>[] enumConstantsTab = (Enum<?>[]) fieldClass.getEnumConstants(); for (Enum<?> o : Arrays.asList(enumConstantsTab)) { enumNames.add(o.name()); } } return enumNames; } public AccessibleClass parseAccessibleClass(Class<?> clazz) { if (DEBUG) { LOG.debug("Parsing accessible class : " + clazz.getName()); } Method[] methods = null; try { methods = clazz.getMethods(); } catch (NoClassDefFoundError e) { LOG.warn("Could not get methods for clazz " + clazz.getName()); return null; } AccessibleClassBean accessClass = createBaseAccessibleClass(clazz); if (Modifier.isAbstract(clazz.getModifiers())) { accessClass.setAbstract(true); } setAccessibleClassInheritance(clazz, accessClass); setAccessibleClassConstructors(clazz, accessClass); Map<String, AccessibleField> exposedAttributes = new HashMap<String, AccessibleField>(); List<Method> exposedMethods = new ArrayList<Method>(); parseExposedMethodAndAttributes(clazz, methods, exposedAttributes, exposedMethods, true); setAccessibleFieldsDescriptions(accessClass, clazz, exposedAttributes); if (isTryInstantiateClassForDefaultValue()) { parseAccessibleFieldsDefaultValue(accessClass, clazz, exposedAttributes); } setAccessibleMethods(accessClass, exposedMethods); return accessClass; } /** * Method to parse default value associated with the field * * @param accessClass * @param clazz * @param exposedAttributes */ private void parseAccessibleFieldsDefaultValue(AccessibleClassBean accessClass, Class<?> clazz, Map<String, AccessibleField> exposedAttributes) { Object o = instanciateObject(clazz); if (o == null) { return; } for (AccessibleField field : exposedAttributes.values()) { if (isPrimitiveType(field.getClassName())) { LOG.debug("Field " + field.getName() + " is a primitive type : " + field.getClassName()); Object defaultValue = getDefaultValueForAttribute(clazz, o, field); if (defaultValue != null) { LOG.debug("Field " + field.getName() + " has default value to " + defaultValue); ((AccessibleFieldBean) field).setDefaultValue(defaultValue.toString()); } } else { LOG.debug("Skipping field " + field.getName() + ", unsupported type " + field.getClassName() + " for default value retrieving"); } } } /** * Method to retrieve default value for attribute * * @param clazz * @param o * @param field * @return */ private Object getDefaultValueForAttribute(Class<?> clazz, Object o, AccessibleField field) { Object result = null; try { Method getter = clazz.getMethod(attributeToGetter(field.getName())); result = getter.invoke(o); } catch (NoSuchMethodException e) { LOG.warn("NoSuchMethodException for attribute : " + field.getName()); } catch (SecurityException e) { LOG.warn("SecurityException : " + field.getName()); } catch (IllegalAccessException e) { LOG.warn("IllegalAccessException : " + field.getName()); } catch (IllegalArgumentException e) { LOG.warn("IllegalArgumentException : " + field.getName()); } catch (InvocationTargetException e) { LOG.warn("InvocationTargetException : " + field.getName()); } finally { LOG.debug("Finally : result=" + result); } return result; } /** * Method to instantiate specific class * * @param clazz * @return */ private Object instanciateObject(Class<?> clazz) { Object o = null; try { o = (Object) clazz.newInstance(); } catch (InstantiationException e) { LOG.debug("InstanciationException for class : " + clazz.getName(), e); } catch (IllegalAccessException e) { LOG.warn("IllegalAccessException for class : " + clazz.getName(), e); } catch (UnsupportedOperationException e) { LOG.warn("IllegalAccessException for class : " + clazz.getName(), e); } catch (RuntimeException e) { LOG.warn("IllegalAccessException for class : " + clazz.getName(), e); } return o; } private void setAccessibleMethods(AccessibleClassBean accessClass, List<Method> exposedMethods) { List<AccessibleMethod> accessibleMethods = new ArrayList<AccessibleMethod>(); for (Method method : exposedMethods) { if (isIgnoredMethod(method)) { continue; } AccessibleMethodBean accessibleMethod = new AccessibleMethodBean(); accessibleMethod.setName(method.getName()); accessibleMethods.add(accessibleMethod); } accessClass.setAccessibleMethods(accessibleMethods); } private void setAccessibleFieldsDescriptions(AccessibleClassBean accessibleClass, Class<?> clazz, Map<String, AccessibleField> exposedAttributes) { for (AccessibleField accessibleField : exposedAttributes.values()) { /** * We make an ugly cast because we do not want to expose setters in * the AccessibleField interface. */ ((AccessibleFieldBean) accessibleField).setClassName(normalizeClassName(accessibleField.getClassName())); for (Class<?> superclass = clazz; superclass != null; superclass = superclass.getSuperclass()) { try { Field field = superclass.getDeclaredField(accessibleField.getName()); ((AccessibleFieldBean) accessibleField).setDescription(getFieldDescription(field)); ((AccessibleFieldBean) accessibleField).setMandatory(getFieldMandatory(field)); ((AccessibleFieldBean) accessibleField).setEnumProperty(getAccessibleEnums(accessibleClass, field)); break; } catch (SecurityException e) { LOG.debug("Could not fetch field '" + accessibleField.getName() + "'"); } catch (NoSuchFieldException e) { LOG.debug("Could not fetch field '" + accessibleField.getName() + "' from class " + superclass.getName()); } catch (NoClassDefFoundError e) { LOG.debug("Could not fetch field '" + accessibleField.getName() + "' from class " + superclass.getName()); } } } accessibleClass.setAccessibleFields(exposedAttributes); } private boolean getAccessibleEnums(AccessibleClassBean accessibleClass, Field field) { String fieldType = field.getType().getName(); if (accessibleClass.containsEnum(fieldType)) { LOG.debug("Enum " + fieldType + " is already loaded in accesibleEnums"); return true; } List<String> enumValues = getFieldEnumValue(field); if (!enumValues.isEmpty()) { LOG.debug("Add enum " + fieldType + " to accessibleClass " + accessibleClass.getName()); accessibleClass.putAccessibleEnum(fieldType, enumValues); return true; } return false; } private void setAccessibleClassConstructors(Class<?> clazz, AccessibleClassBean accessClass) { for (Constructor<?> constructor : clazz.getConstructors()) { AccessibleConstructorBean mConstructor = new AccessibleConstructorBean(); mConstructor.setArgumentTypes(new ArrayList<String>()); for (Class<?> arg : constructor.getParameterTypes()) { mConstructor.getArgumentTypes().add(normalizeClassName(arg.getName())); } accessClass.getConstructors().add(mConstructor); } } private void setAccessibleClassInheritance(Class<?> clazz, AccessibleClassBean accessClass) { for (Class<?> itf : clazz.getInterfaces()) { accessClass.getInterfaces().add(normalizeClassName(itf.getName())); } for (Class<?> superClass = clazz; superClass != null; superClass = superClass.getSuperclass()) { for (Class<?> itf : superClass.getInterfaces()) { accessClass.getAllInterfaces().add(normalizeClassName(itf.getName())); } } } private AccessibleClassBean createBaseAccessibleClass(Class<?> clazz) { AccessibleClassBean accessClass = new AccessibleClassBean(); accessClass.setAllInterfaces(new ArrayList<String>()); accessClass.setInterfaces(new ArrayList<String>()); accessClass.setConstructors(new ArrayList<AccessibleConstructor>()); accessClass.getInterfaces().add(java.lang.Object.class.getName()); accessClass.getAllInterfaces().add(java.lang.Object.class.getName()); accessClass.setName(normalizeClassName(clazz.getName())); accessClass.setDescription(getClassDescription(clazz)); if (clazz.getSuperclass() == null) { LOG.debug("No superclass for class : '" + clazz.getName() + "'"); } else { accessClass.setSuperclass(clazz.getSuperclass().getName()); } return accessClass; } private String normalizeClassName(final String className) { if (isReplaceDollarByPointForEmbeddedClasses()) { return className.replace('$', '.'); } else { return className; } } public boolean isPrimitiveType(String className) { return PrimitiveTypeUtil.isPrimitiveType(className); } public boolean isReplaceDollarByPointForEmbeddedClasses() { return replaceDollarByPointForEmbeddedClasses; } public void setReplaceDollarByPointForEmbeddedClasses(boolean replaceDollarByPointForEmbeddedClasses) { this.replaceDollarByPointForEmbeddedClasses = replaceDollarByPointForEmbeddedClasses; } public boolean isTryInstantiateClassForDefaultValue() { return tryInstantiateClassForDefaultValue; } public void setTryInstantiateClassForDefaultValue(boolean tryInstantiateClassForDefaultValue) { this.tryInstantiateClassForDefaultValue = tryInstantiateClassForDefaultValue; } }