Java tutorial
/******************************************************************************* * Copyright () 2009, 2011 David Wong * * This file is part of TestDataCaptureJ. * * TestDataCaptureJ is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * TestDataCaptureJ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Afferro General Public License for more details. * * You should have received a copy of the GNU Afferro General Public License * along with TestDataCaptureJ. If not, see <http://www.gnu.org/licenses/>. *******************************************************************************/ package au.com.dw.testdatacapturej.reflection; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.collections.iterators.ArrayIterator; import au.com.dw.testdatacapturej.builder.BuilderUtil; import au.com.dw.testdatacapturej.config.CollectionAdderConfig; import au.com.dw.testdatacapturej.config.ConfigUtil; import au.com.dw.testdatacapturej.meta.ContainmentType; import au.com.dw.testdatacapturej.meta.ObjectInfo; import au.com.dw.testdatacapturej.meta.ObjectType; import au.com.dw.testdatacapturej.meta.SetterGenerationType; import au.com.dw.testdatacapturej.reflection.util.ReflectionUtil; import au.com.dw.testdatacapturej.util.TypeUtil; /** * Implementation of ReflectionHandler that generates meta-data information about objects. This meta-data * can be used to generate the test code for the objects. * * Use the log(..) method to start the process for the initial object (i.e. parameter or * return value). * * @author David Wong * */ public class MetadataGenerationHandler implements ReflectionHandler { /** * Entry into the reflective meta-data generation process. Determines the type of the object and then * passes to the appropriate handler methods. The handler methods are recursive so if any of the objects * are classes with fields or container classes such as Collections and Arrays, then the child or element * objects will also be passed to the appropriate handler method. * * For each object, whether it is the initial object or a child object, an ObjectInfo is created to store * the meta-data for that object. This meta-data will later will used to determine what test code is generated * for each object. * * This initial object can't be null, so any null check should be done before this method is called. * * @param initialObject * @throws IllegalArgumentException * @throws IllegalAccessException * * @see au.com.dw.testdatacapturej.meta.ObjectInfo */ public ObjectInfo handle(Object initialObject) throws IllegalArgumentException, IllegalAccessException { ObjectInfo info = new ObjectInfo(); info.setValue(initialObject); info.setInitalObject(true); info.setContainmentType(ContainmentType.NONE); info.getConstructorInfo().setHasDefaultConstructor(hasDefaultConstructor(info.getValue())); if (TypeUtil.isJavaClass(initialObject)) { info.setType(ObjectType.SIMPLE); } else { String fieldName; Class<?> clazz = initialObject.getClass(); if (TypeUtil.isArray(initialObject)) { fieldName = ReflectionUtil.ARGUMENT_ARRAY_FIELD_NAME; info.setType(ObjectType.ARRAY); // special handling for array class names and field names info.setClassName(ReflectionUtil.getArrayClassName(initialObject)); String arrayType = initialObject.getClass().getComponentType().getName(); info.setClassFieldName(BuilderUtil.createArrayClassFieldName(arrayType, null, null)); handleArray(info); } else if (TypeUtil.isCollection(initialObject)) { fieldName = ReflectionUtil.ARGUMENT_COLLECTION_FIELD_NAME; info.setType(ObjectType.COLLECTION); info.setClassName(clazz.getName()); info.setClassFieldName(BuilderUtil.createClassFieldName(initialObject, null, null)); handleCollection(info); } else if (TypeUtil.isMap(initialObject)) { fieldName = ReflectionUtil.ARGUMENT_MAP_FIELD_NAME; info.setType(ObjectType.MAP); info.setClassName(clazz.getName()); info.setClassFieldName(BuilderUtil.createClassFieldName(initialObject, null, null)); handleMap(info); } else { fieldName = ReflectionUtil.ARGUMENT_OBJECT_FIELD_NAME; info.setType(ObjectType.OBJECT); info.setClassName(clazz.getName()); info.setClassFieldName(BuilderUtil.createClassFieldName(initialObject, null, null)); handleFields(info); // check if configured parameterized constructor is to be used for the initial object ConfigUtil configUtil = new ConfigUtil(); info.getConstructorInfo().addConstructorParamFieldNames(configUtil.getConstructionParameters(info)); } info.setFieldName(fieldName); } return info; } /** * Use reflection to process the fields in an object. Iterates through the fields in the object * and passes them to the appropriate handlers. * * Note: does not handle static fields yet. * * @param object * @throws IllegalArgumentException * @throws IllegalAccessException */ protected void handleFields(ObjectInfo info) throws IllegalArgumentException, IllegalAccessException { // use reflection to get the fields of the class Object object = info.getValue(); Class<?> clazz = object.getClass(); Field[] fields = clazz.getDeclaredFields(); AccessibleObject.setAccessible(fields, true); // check configuration for non-standard handling ConfigUtil configUtil = new ConfigUtil(); // get list of field names that have been configured to have non-standard setter method generation List<String> ignoredSetterFieldNames = configUtil.getIgnoredSetters(info); // get list of collection field names for collection that are only accessed through adder methods List<CollectionAdderConfig> collectionConfigs = configUtil.getAddedCollections(info); for (int i = 0; i < fields.length; i++) { Field field = fields[i]; int mod = field.getModifiers(); // ignore static fields if (!Modifier.isStatic(mod)) { // get the field info String fieldName = field.getName(); Object fieldValue = field.get(object); // create new ObjectInfo for the field object ObjectInfo fieldInfo = new ObjectInfo(); fieldInfo.setFieldName(fieldName); fieldInfo.setValue(fieldValue); fieldInfo.setContainingClassFieldName(info.getClassFieldName()); fieldInfo.getConstructorInfo().setHasDefaultConstructor(hasDefaultConstructor(fieldValue)); // check if requires any special setter method generation if (ignoredSetterFieldNames != null) { if (ignoredSetterFieldNames.contains(fieldName)) { fieldInfo.getSetterAdderInfo().setSetterGenerationType(SetterGenerationType.IGNORE); } } // determine type of field and pass to handler if (fieldValue != null) { if (!ReflectionUtil.hasSetterMethod(object, fieldName, fieldValue)) { fieldInfo.getSetterAdderInfo().setHasSetter(false); } if (TypeUtil.isJavaClass(fieldValue)) { fieldInfo.setType(ObjectType.SIMPLE); fieldInfo.setClassName(fieldValue.getClass().getName()); fieldInfo.setClassFieldName(BuilderUtil.createClassFieldName(fieldValue, null, null)); } else if (TypeUtil.isArray(fieldValue)) { fieldInfo.setType(ObjectType.ARRAY); // special handling for array class names fieldInfo.setClassName(ReflectionUtil.getArrayClassName(fieldValue)); String arrayType = fieldValue.getClass().getComponentType().getName(); fieldInfo.setClassFieldName(BuilderUtil.createArrayClassFieldName(arrayType, null, null)); if (!fieldInfo.isSetterIgnoreType()) { handleArray(fieldInfo); } } else if (TypeUtil.isCollection(fieldValue)) { fieldInfo.setType(ObjectType.COLLECTION); fieldInfo.setClassName(fieldValue.getClass().getName()); fieldInfo.setClassFieldName(BuilderUtil.createClassFieldName(fieldValue, null, null)); // check if the collection field is only accessed through adder methods // Note: the adder check overrides the ignored setter check if both are set CollectionAdderConfig foundConfig = configUtil.getCollectionAdderConfig(collectionConfigs, fieldName); if (foundConfig != null) { fieldInfo.getSetterAdderInfo().setUsesAdder(true); fieldInfo.getSetterAdderInfo().setAdderMethodName(foundConfig.getAdderMethodName()); handleCollection(fieldInfo); } else if (!fieldInfo.isSetterIgnoreType()) { handleCollection(fieldInfo); } } else if (TypeUtil.isMap(fieldValue)) { fieldInfo.setType(ObjectType.MAP); fieldInfo.setClassName(fieldValue.getClass().getName()); fieldInfo.setClassFieldName(BuilderUtil.createClassFieldName(fieldValue, null, null)); if (!fieldInfo.isSetterIgnoreType()) { handleMap(fieldInfo); } } else { fieldInfo.setType(ObjectType.OBJECT); fieldInfo.setClassName(fieldValue.getClass().getName()); fieldInfo.setClassFieldName(BuilderUtil.createClassFieldName(fieldValue, null, null)); if (!fieldInfo.isSetterIgnoreType()) { handleFields(fieldInfo); } } } else { // get class name from the Field if the field value is null fieldInfo.setClassName(ReflectionUtil.getClassNameFromField(field)); fieldInfo.setClassFieldName(BuilderUtil.createClassFieldName(field, null, null)); fieldInfo.setType(ObjectType.SIMPLE); } // check if configured parameterized constructor is to be used for the field fieldInfo.getConstructorInfo() .addConstructorParamFieldNames(configUtil.getConstructionParameters(fieldInfo)); // add the parent class to field class link fieldInfo.setParentInfo(info); info.addFieldToList(fieldInfo); } } } /** * The handler for array classes. Determines the type of objects * inside the array and passes them to the appropriate handler. * * @param array * @throws IllegalAccessException */ protected void handleArray(ObjectInfo info) throws IllegalAccessException { ArrayIterator iter = new ArrayIterator(info.getValue()); int index = 0; ContainmentType elementType = ContainmentType.ARRAY_ELEMENT; // handle each array element while (iter.hasNext()) { Object elementObject = iter.next(); ObjectInfo elementInfo = new ObjectInfo(); // increment the array index so each array element can later be assigned to successive array slots elementInfo.setIndex(index++); if (elementObject != null) { elementInfo.setValue(elementObject); elementInfo.setContainingClassFieldName(info.getClassFieldName()); elementInfo.getConstructorInfo().setHasDefaultConstructor(hasDefaultConstructor(elementObject)); // no need to check for setter since is an element of an array, so would be assigned instead elementInfo.getSetterAdderInfo().setHasSetter(false); if (TypeUtil.isJavaClass(elementObject)) { elementInfo.setType(ObjectType.SIMPLE); elementInfo.setContainmentType(elementType); elementInfo.setClassName(elementObject.getClass().getName()); elementInfo.setClassFieldName(BuilderUtil.createClassFieldName(elementObject, null, null)); } else if (TypeUtil.isArray(elementObject)) { elementInfo.setType(ObjectType.ARRAY); elementInfo.setContainmentType(elementType); // special handling for array class names elementInfo.setClassName(ReflectionUtil.getArrayClassName(elementObject)); String arrayType = elementObject.getClass().getComponentType().getName(); elementInfo.setClassFieldName(BuilderUtil.createArrayClassFieldName(arrayType, null, null)); handleArray(elementInfo); } else if (TypeUtil.isMap(elementObject)) { elementInfo.setType(ObjectType.MAP); elementInfo.setContainmentType(elementType); elementInfo.setClassName(elementObject.getClass().getName()); elementInfo.setClassFieldName(BuilderUtil.createClassFieldName(elementObject, null, null)); handleMap(elementInfo); } else if (TypeUtil.isCollection(elementObject)) { elementInfo.setType(ObjectType.COLLECTION); elementInfo.setContainmentType(elementType); elementInfo.setClassName(elementObject.getClass().getName()); elementInfo.setClassFieldName(BuilderUtil.createClassFieldName(elementObject, null, null)); handleCollection(elementInfo); } else { elementInfo.setType(ObjectType.OBJECT); elementInfo.setContainmentType(elementType); elementInfo.setClassName(elementObject.getClass().getName()); elementInfo.setClassFieldName(BuilderUtil.createClassFieldName(elementObject, null, null)); handleFields(elementInfo); } } else { elementInfo.setType(ObjectType.SIMPLE); elementInfo.setContainmentType(elementType); } // add the link between the array and each element elementInfo.setParentInfo(info); info.addFieldToList(elementInfo); } } /** * The handler for Collection implementation classes. Determines the type of objects * inside the Collection and passes them to the appropriate handler. * * Note that different implementations of Collection will allow / disallow null value for elements. * * @param fieldName * @param collection * @throws IllegalAccessException */ protected void handleCollection(ObjectInfo info) throws IllegalAccessException { Collection<?> collection = (Collection<?>) info.getValue(); // determine whether the collection is accessed through an adder method in it's containing class ContainmentType elementType = info.getSetterAdderInfo().isUsesAdder() ? ContainmentType.ADDED_COLLECTION_ELEMENT : ContainmentType.COLLECTION_ELEMENT; // handle each collection element for (Object elementObject : collection) { ObjectInfo elementInfo = new ObjectInfo(); if (elementObject != null) { elementInfo.setValue(elementObject); elementInfo.setContainingClassFieldName(info.getClassFieldName()); elementInfo.getConstructorInfo().setHasDefaultConstructor(hasDefaultConstructor(elementObject)); // no need to check for setter since is an element of a collection, so would be added to the collection elementInfo.getSetterAdderInfo().setHasSetter(false); if (TypeUtil.isJavaClass(elementObject)) { elementInfo.setType(ObjectType.SIMPLE); elementInfo.setContainmentType(elementType); elementInfo.setClassName(elementObject.getClass().getName()); elementInfo.setClassFieldName(BuilderUtil.createClassFieldName(elementObject, null, null)); } else if (TypeUtil.isArray(elementObject)) { elementInfo.setType(ObjectType.ARRAY); elementInfo.setContainmentType(elementType); // special handling for array class names elementInfo.setClassName(ReflectionUtil.getArrayClassName(elementObject)); String arrayType = elementObject.getClass().getComponentType().getName(); elementInfo.setClassFieldName(BuilderUtil.createArrayClassFieldName(arrayType, null, null)); handleArray(elementInfo); } else if (TypeUtil.isCollection(elementObject)) { elementInfo.setType(ObjectType.COLLECTION); elementInfo.setContainmentType(elementType); elementInfo.setClassName(elementObject.getClass().getName()); elementInfo.setClassFieldName(BuilderUtil.createClassFieldName(elementObject, null, null)); handleCollection(elementInfo); } else if (TypeUtil.isMap(elementObject)) { elementInfo.setType(ObjectType.MAP); elementInfo.setContainmentType(elementType); elementInfo.setClassName(elementObject.getClass().getName()); elementInfo.setClassFieldName(BuilderUtil.createClassFieldName(elementObject, null, null)); handleMap(elementInfo); } else { elementInfo.setType(ObjectType.OBJECT); elementInfo.setContainmentType(elementType); elementInfo.setClassName(elementObject.getClass().getName()); elementInfo.setClassFieldName(BuilderUtil.createClassFieldName(elementObject, null, null)); handleFields(elementInfo); } } else { // for Collection implementations that allow null elements elementInfo.setType(ObjectType.SIMPLE); elementInfo.setContainmentType(elementType); } // add the link between the collection and the contained elements elementInfo.setParentInfo(info); info.addFieldToList(elementInfo); } } /** * The handler for Map implementation classes. Determines the type of objects * inside the Map (both key and value) and passes them to the appropriate handler. * * Note that different implementations of Map will allow / disallow null values for the key * and / or value. * * @param map * @throws IllegalAccessException */ protected void handleMap(ObjectInfo info) throws IllegalAccessException { Map map = (Map) info.getValue(); ContainmentType elementType = ContainmentType.MAP_ENTRY; // handle each map entry, doing both the key and value for each entry Set<Map.Entry<?, ?>> entrySet = map.entrySet(); for (Map.Entry entry : entrySet) { Object key = entry.getKey(); Object value = entry.getValue(); ObjectInfo valueInfo = new ObjectInfo(); ObjectInfo keyInfo = new ObjectInfo(); // handle the map key if (key != null) { keyInfo.setValue(key); keyInfo.setContainingClassFieldName(info.getClassFieldName()); keyInfo.getConstructorInfo().setHasDefaultConstructor(hasDefaultConstructor(key)); // no need to check for setter since is a part of an entry to a map, so would be put // into the map instead keyInfo.getSetterAdderInfo().setHasSetter(false); if (TypeUtil.isJavaClass(key)) { keyInfo.setType(ObjectType.SIMPLE); keyInfo.setClassName(key.getClass().getName()); keyInfo.setClassFieldName(BuilderUtil.createClassFieldName(key, null, null)); } else if (TypeUtil.isArray(key)) { keyInfo.setType(ObjectType.ARRAY); // special handling for array class names keyInfo.setClassName(ReflectionUtil.getArrayClassName(key)); String arrayType = key.getClass().getComponentType().getName(); keyInfo.setClassFieldName(BuilderUtil.createArrayClassFieldName(arrayType, null, null)); handleArray(keyInfo); } else if (TypeUtil.isCollection(key)) { keyInfo.setType(ObjectType.COLLECTION); keyInfo.setClassName(key.getClass().getName()); keyInfo.setClassFieldName(BuilderUtil.createClassFieldName(key, null, null)); handleCollection(keyInfo); } else if (TypeUtil.isMap(key)) { keyInfo.setType(ObjectType.MAP); keyInfo.setClassName(key.getClass().getName()); keyInfo.setClassFieldName(BuilderUtil.createClassFieldName(key, null, null)); handleMap(keyInfo); } else { keyInfo.setType(ObjectType.OBJECT); keyInfo.setClassName(key.getClass().getName()); keyInfo.setClassFieldName(BuilderUtil.createClassFieldName(key, null, null)); handleFields(keyInfo); } } else { // for Map implementations that allow null for key keyInfo.setType(ObjectType.SIMPLE); } // handle the map value if (value != null) { valueInfo.setValue(value); valueInfo.setContainingClassFieldName(info.getClassFieldName()); valueInfo.getConstructorInfo().setHasDefaultConstructor(hasDefaultConstructor(value)); // no need to check for setter since is a part of an entry to a map, so would be put // into the map instead valueInfo.getSetterAdderInfo().setHasSetter(false); if (TypeUtil.isJavaClass(value)) { valueInfo.setType(ObjectType.SIMPLE); valueInfo.setContainmentType(elementType); valueInfo.setClassName(value.getClass().getName()); valueInfo.setClassFieldName(BuilderUtil.createClassFieldName(value, null, null)); } else if (TypeUtil.isArray(value)) { valueInfo.setType(ObjectType.ARRAY); valueInfo.setContainmentType(elementType); // special handling for array class names valueInfo.setClassName(ReflectionUtil.getArrayClassName(value)); String arrayType = value.getClass().getComponentType().getName(); valueInfo.setClassFieldName(BuilderUtil.createArrayClassFieldName(arrayType, null, null)); handleArray(valueInfo); } else if (TypeUtil.isCollection(value)) { valueInfo.setType(ObjectType.COLLECTION); valueInfo.setContainmentType(elementType); valueInfo.setClassName(value.getClass().getName()); valueInfo.setClassFieldName(BuilderUtil.createClassFieldName(value, null, null)); handleCollection(valueInfo); } else if (TypeUtil.isMap(value)) { valueInfo.setType(ObjectType.MAP); valueInfo.setContainmentType(elementType); valueInfo.setClassName(value.getClass().getName()); valueInfo.setClassFieldName(BuilderUtil.createClassFieldName(value, null, null)); handleMap(valueInfo); } else { valueInfo.setType(ObjectType.OBJECT); valueInfo.setContainmentType(elementType); valueInfo.setClassName(value.getClass().getName()); valueInfo.setClassFieldName(BuilderUtil.createClassFieldName(value, null, null)); handleFields(valueInfo); } } else { // for Map implementations that allow null for value valueInfo.setType(ObjectType.SIMPLE); valueInfo.setContainmentType(elementType); } // add the link between the key and the value, and also between the map the the contained entries valueInfo.setKeyInfo(keyInfo); valueInfo.setParentInfo(info); info.addFieldToList(valueInfo); } } /** * Check if a class has a default no-argument constructor. * * @param value * @return */ private boolean hasDefaultConstructor(Object value) { boolean hasDefaultConstructor = false; // no need to check for default constructor for null value, since it doesn't need to be constructed if (value != null) { // simple types don't need to be constructed if (!TypeUtil.isJavaClass(value)) { // arrays use a different constructor if (!TypeUtil.isArray(value)) { hasDefaultConstructor = ReflectionUtil.hasDefaultConstructor(value); } } } return hasDefaultConstructor; } }